/**
 * @class ST.future.Component
 * @extend ST.future.Element
 * This class is used to manage an Ext JS Component (`Ext.Component`) that will exist at
 * some point in the future. The inherited features of {@link ST.future.Element} all
 * operate on the component's primary element.
 *
 * The general mechanics of futures is covered in {@link ST.future.Element}.
 *
 * This class extends its base to provide additional `Ext.Component` specific action
 * and state methods.
 *
 * ### Note
 *
 * This class is not created directly by user code. Instead, it is created automatically
 * by {@link ST#component} or one of the more specific factory methods:
 *
 *  - `{@link ST#button}`
 *  - `{@link ST#checkBox}`
 *  - `{@link ST#comboBox}`
 *  - `{@link ST#dataView}`
 *  - `{@link ST#field}`
 *  - `{@link ST#grid}`
 *  - `{@link ST#panel}`
 *  - `{@link ST#picker}`
 *  - `{@link ST#textField}`
 *
 */
ST.future.define('Component', {
    extend: ST.future.Element,
    valueProperty: 'cmp',
 
    playables: {
        // TODO - focusentered
        // TODO - focusleft
 
        /**
         * @method destroyed
         * Waits for this component to be destroyed.
         *
         *      ST.component('@someCmp').
         *          destroyed().
         *          and(function (cmp) {
         *              // cmp is now destroyed
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Component} this
         * @chainable
         */
        destroyed: {
            remoteable: false,
            available: null,
            visibility: null,
 
            is: function () {
                var cmp = this.getComponent();
                return cmp ? (cmp.destroyed || cmp.isDestroyed) : true;
            },
 
            wait: 'destroy'
        },
 
        /**
         * @method disabled
         * Waits for this component to be disabled.
         *
         *      ST.component('@someCmp').
         *          disabled().
         *          and(function (cmp) {
         *              // cmp is now disabled
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Component} this
         * @chainable
         */
        disabled: {
            is: function () {
                var cmp = this.getComponent();
                return ST.isClassic ? cmp.disabled : cmp.getDisabled();
            },
            wait: ['disable', 'disabledchange']
        },
 
        /**
         * @method enabled
         * Waits for this component to be enabled.
         *
         *      ST.component('@someCmp').
         *          enabled().
         *          and(function (cmp) {
         *              // cmp is now enabled
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Component} this
         * @chainable
         */
        enabled: {
            is: function () {
                var cmp = this.getComponent();
                return ST.isClassic ? !cmp.disabled : !cmp.getDisabled();
            },
            wait: ['enable', 'disabledchange']
        },
 
        /**
         * @method rendered
         * Waits for this component to be rendered. This wait method only works when
         * the `ST.component` method is given an un-rendered Component instance. If a
         * locator string is used, the `ST.component` method will implicitly wait for
         * the Component's element to be present in the DOM (i.e., the component is in
         * a rendered state).
         *
         * Since this wait is normally handled by the `ST.component` method, this wait
         * method is seldom needed.
         *
         *      ST.component(comp).  // comp is an unrendered Component instance
         *          rendered().
         *          and(function () {
         *              // comp is now rendered
         *          });
         *
         * NOTE: This method is not supported for WebDriver-based tests.
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Component} this
         * @chainable
         */
        rendered: {
            is: function () {
                var cmp = this.getComponent();
                return !ST.isClassic || cmp.rendered;
            },
            wait: 'afterrender'
        },
 
        /**
         * @method get
         * Retrieves the values of the specified list of properties from the future's 
         * underlying component or dom element. These values will then be available on the future itself 
         * (for example, to be used in expectations within an and() method).
         *
         * This method is particularly useful for WebDriver-based tests where direct access to Ext JS
         * components is not possible within the context of the executed spec.
         *
         *      ST.component('@futureCmp')
         *          .get('height')
         *          .and(function () {
         *              expect(this.future.data.height).toBe(200);
         *          })
         * 
         * The property names will be used to get information from the component or underlying dom element
         * according to the following priority.
         * 
         * For the property name 'foo':
         * 
         *      cmp.getFoo()
         *      cmp.foo
         *      cmp.el.dom.foo
         *      cmp.el.getStyle('foo')
         *
         * Investigating deep into the structure of a component is not supported. If you wish to interrogate
         * the structure of the component use either the {@link ST.future.Element#and} or {@link ST.future.Element#execute}
         * methods.
         *  
         * @param {String} properties A comma-delimited list of property values to retrieive
         * @return {ST.future.Component} this
         * @chainable
         */
        get: {
            params: 'names,timeout',
            fn: function () {
                var me = this,
                    future = me.future,
                    names = me.args.names.split(','),
                    cmp = me.getComponent(),
                    el = me.getElement(),
                    len = names.length, 
                    i, key, val, getter;
 
                future.data = future.data || {};
 
                if (cmp) {
                    for (i = 0; i < len; i++) {
                        val = undefined;
                        key = names[i];
                        getter = cmp[ST.future.Element.createGetter(key)];
 
                        if (getter) {
                            val = getter.call(cmp);
                        }
                        if (val === undefined) {
                            val = cmp[key];
                        }
                        if (val === undefined && el.dom) {
                            val = el.dom[key];
                        }
                        if (val === undefined) {
                            val = el.getStyle(key);
                        }
                        if (val !== undefined) {
                            future.data[key] = val;
                        }
                    }
                }
            }
        },
        /**
         * @method disable
         * Schedules this component to be disabled.
         *
         *      ST.component('@someCmp').
         *          disable().
         *          and(function (cmp) {
         *              // cmp has now been disabled.
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Component} this
         * @chainable
         */
        disable: {
            params: 'timeout',
            fn: function () {
                this.getComponent().disable();
            }
        },
 
        /**
         * @method enable
         * Schedules this component to be enabled.
         *
         *      ST.component('@someCmp').
         *          enable().
         *          and(function (cmp) {
         *              // cmp has now been enabled.
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Component} this
         * @chainable
         */
        enable: {
            params: 'timeout',
            fn: function () {
                this.getComponent().enable();
            }
        },
 
        /**
         * @method fireEvent
         * 
         * Calls the fireEvent method on this future's component with the given
         * event name. Passing other arguments to the fireEvent method is not
         * supported at this time.
         * 
         * This was introduced to help in white-box style testing for WebDriver
         * scenarios. So that we could put special test-related event handlers
         * on components and fire them from a test.
         * 
         *      Ext.define('my.Text', {
         *          extend: 'Ext.form.field.Text',
         *          xtype: 'my-text',
         *          id: 'someCmp',
         *          fieldLable: 'My Text Field',
         *          listeners: {
         *              test: function () {
         *                  this.setValue('test-value-set');
         *              }
         *          }
         *      };
         * 
         *      ST.component('@someCmp')
         *          .fireEvent('test')
         *          .value('test-value-set');
         * 
         * @chainable
         * @return {ST.future.Component} this
         * @since 2.0.0
         */
        fireEvent: {
            params: 'eventname',
            fn: function () {
                this.future.cmp.fireEvent(this.args.eventname);
            }
        },
 
        /**
         * @private
         */
        asComponent: {
            addEvent: function (config, timeout) {
                var me = this;
 
                me.setRelated('item', config.item);
 
                me.locator = me.play(me._buildRec('asComponent', {
                    visible: null,
                    animation: false,
                    timeout: timeout
                }));
 
                return this;
            }
        }
    },
 
    constructor: function (config) {
        ST.future.Component.superclass.constructor.call(this, config);
    },
 
    /**
     * @method down
     * Returns a descendant `{@link ST.future.Element future element}` that corresponds to the selector.
     *
     *      ST.component('@someCmp').
     *          down('panel >> .link').
     *          and(function (element) {
     *              // element is now available
     *          });
     *
     * If the specified selector cannot be resolved, the request will timeout.
     * @param {String} selector The DOM or Composite query selector to use to search for the element
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Element}
     * @chainable
     */
    down: function (selector, timeout) {
        return this._createRelatedFuture('ST.future.Element', 'down', selector, timeout);
    },
 
    /**
     * @method up
     * Returns an ancestor `{@link ST.future.Element future element}` that corresponds to the selector.
     *
     *      ST.component('@someCmp').
     *          up('.wrapper').
     *          and(function (element) {
     *              // element is now available
     *          });
     *
     * If the specified selector cannot be resolved, the request will timeout.
     * @param {String} selector The DOM or Composite query selector to use to search for the element
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Element}
     * @chainable
     */
    up: function (selector, timeout) {
        return this._createRelatedFuture('ST.future.Element', 'up', selector, timeout);
    },
 
    /**
     * @method child
     * Returns a direct child `{@link ST.future.Element future element}` that corresponds to the selector.
     *
     *      ST.component('@someCmp').
     *          child('.x-button').
     *          and(function (element) {
     *              // element is now available
     *          });
     *
     * If the specified selector cannot be resolved, the request will timeout.
     * @param {String} selector The DOM or Composite query selector to use to search for the element
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Element}
     * @chainable
     */
    child: function (selector, timeout) {
        return this._createRelatedFuture('ST.future.Element', 'child', selector, timeout);
    },
 
    /**
     * @method gotoComponent
     * Returns a `{@ST.future.Component}` future component that is hierarchically-related to the current component future
     *
     *      ST.component('@someCmp').
     *          goToComponent('container').
     *          and(function (container) {
     *              // container is now available
     *          });
     *
     * @param {String} selector The Component Query selector.
     * @param {"down"/"up"/"child"} [direction="down"] The direction of relationship.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Component}
     * @chainable
     */
    gotoComponent: function (selector, direction, timeout) {
        return this._goto('ST.future.Component', direction, selector, timeout);
    },
 
    /**
     * @method gotoButton
     * Returns a `{@ST.future.Button}` future component that is hierarchically-related to the current component future
     *
     *      ST.component('@someCmp').
     *          gotoButton('button').
     *          and(function (button) {
     *              // button is now available
     *          });
     *
     * @param {String} selector The Component Query selector.
     * @param {"down"/"up"/"child"} [direction="down"] The direction of relationship.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Button}
     * @chainable
     */
    gotoButton: function (selector, direction, timeout) {
        return this._goto('ST.future.Button', direction, selector, timeout);
    },
 
    /**
     * @method gotoField
     * Returns a `{@ST.future.Field}` future component that is hierarchically-related to the current component future
     *
     *      ST.component('@someCmp').
     *          gotoField('field').
     *          and(function (field) {
     *              // field is now available
     *          });
     *
     * @param {String} selector The Component Query selector.
     * @param {"down"/"up"/"child"} [direction="down"] The direction of relationship.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Field}
     * @chainable
     */
    gotoField: function (selector, direction, timeout) {
        return this._goto('ST.future.Field', direction, selector, timeout);
    },
 
    /**
     * @method gotoCheckBox
     * Returns a `{@ST.future.CheckBox}` future component that is hierarchically-related to the current component future
     *
     *      ST.component('@someCmp').
     *          gotoCheckBox('checkboxfield').
     *          and(function (checkbox) {
     *              // checkbox is now available
     *          });
     *
     * @param {String} selector The Component Query selector.
     * @param {"down"/"up"/"child"} [direction="down"] The direction of relationship.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.CheckBox}
     * @chainable
     */
    gotoCheckBox: function (selector, direction, timeout) {
        return this._goto('ST.future.CheckBox', direction, selector, timeout);
    },
 
    /**
     * @method gotoTextField
     * Returns a `{@ST.future.TextField}` future component that is hierarchically-related to the current component future
     *
     *      ST.component('@someCmp').
     *          gotoTextField('textfield').
     *          and(function (textfield) {
     *              // textfield is now available
     *          });
     *
     * @param {String} selector The Component Query selector.
     * @param {"down"/"up"/"child"} [direction="down"] The direction of relationship.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.TextField}
     * @chainable
     */
    gotoTextField: function (selector, direction, timeout) {
        return this._goto('ST.future.TextField', direction, selector, timeout);
    },
 
    /**
     * @method gotoSlider
     * Returns a `{@ST.future.Slider}` future component that is hierarchically-related to the current component future
     *
     *      ST.component('@someCmp').
     *          gotoSlider('slider').
     *          and(function (slider) {
     *              // slider is now available
     *          });
     *
     * @param {String} selector The Component Query selector.
     * @param {"down"/"up"/"child"} [direction="down"] The direction of relationship.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Slider}
     * @chainable
     */
    gotoSlider: function (selector, direction, timeout) {
        return this._goto('ST.future.Slider', direction, selector, timeout);
    },
 
    /**
     * @method gotoPicker
     * Returns a `{@ST.future.Picker}` future component that is hierarchically-related to the current component future
     *
     *      ST.component('@someCmp').
     *          gotoPicker('pickerfield').
     *          and(function (picker) {
     *              // picker is now available
     *          });
     *
     * @param {String} selector The Component Query selector.
     * @param {"down"/"up"/"child"} [direction="down"] The direction of relationship.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Picker}
     * @chainable
     */
    gotoPicker: function (selector, direction, timeout) {
        return this._goto('ST.future.Picker', direction, selector, timeout);
    },
 
    /**
     * @method gotoComboBox
     * Returns a `{@ST.future.ComboBox}` future component that is hierarchically-related to the current component future
     *
     *      ST.component('@someCmp').
     *          gotoComboBox('combobox').
     *          and(function (combobox) {
     *              // combobox is now available
     *          });
     *
     * @param {String} selector The Component Query selector.
     * @param {"down"/"up"/"child"} [direction="down"] The direction of relationship.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.ComboBox}
     * @chainable
     */
    gotoComboBox: function (selector, direction, timeout) {
        return this._goto('ST.future.ComboBox', direction, selector, timeout);
    },
 
    /**
     * @method gotoSelect
     * Returns a `{@ST.future.Select}` future component that is hierarchically-related to the current component future
     *
     *      ST.component('@someCmp').
     *          gotoSelect('selectfield').
     *          and(function (select) {
     *              // select is now available
     *          });
     *
     * @param {String} selector The Component Query selector.
     * @param {"down"/"up"/"child"} [direction="down"] The direction of relationship.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Select}
     * @chainable
     */
    gotoSelect: function (selector, direction, timeout) {
        return this._goto('ST.future.Select', direction, selector, timeout);
    },
 
    /**
     * @method gotoPanel
     * Returns a `{@ST.future.Panel}` future component that is hierarchically-related to the current component future
     *
     *      ST.component('@someCmp').
     *          gotoPanel('panel').
     *          and(function (panel) {
     *              // panel is now available
     *          });
     *
     * @param {String} selector The Component Query selector.
     * @param {"down"/"up"/"child"} [direction="down"] The direction of relationship.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Panel}
     * @chainable
     */
    gotoPanel: function (selector, direction, timeout) {
        return this._goto('ST.future.Panel', direction, selector, timeout);
    },
 
    /**
     * @method gotoDataView
     * Returns a `{@ST.future.Panel}` future component that is hierarchically-related to the current component future
     *
     *      ST.component('@someCmp').
     *          gotoDataView('dataview').
     *          and(function (dataview) {
     *              // dataview is now available
     *          });
     *
     * @param {String} selector The Component Query selector.
     * @param {"down"/"up"/"child"} [direction="down"] The direction of relationship.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.DataView}
     * @chainable
     */
    gotoDataView: function (selector, direction, timeout) {
        return this._goto('ST.future.DataView', direction, selector, timeout);
    },
 
    /**
     * @method gotoGrid
     * Returns a `{@ST.future.Panel}` future component that is hierarchically-related to the current component future
     *
     *      ST.component('@someCmp').
     *          gotoGrid('grid').
     *          and(function (grid) {
     *              // grid is now available
     *          });
     *
     * @param {String} selector The Component Query selector.
     * @param {"down"/"up"/"child"} [direction="down"] The direction of relationship.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Grid}
     * @chainable
     */
    gotoGrid: function (selector, direction, timeout) {
        return this._goto('ST.future.Grid', direction, selector, timeout);
    },
 
    _goto: function (maker, direction, selector, timeout) {
        return this._createRelatedFuture(maker, direction || 'down', selector, timeout);
    },
 
    // TODO - lookup
 
    /**
     * Returns the owning `ST.future.Item`. This method can be called at any time
     * to "return" to the owning future. For example:
     *
     *      ST.dataView('@someDataView').
     *          item(42).            // get a future item (ST.future.Item)
     *              asButton().      // get item as a button (ST.future.Button)
     *                  press().     // operates on the ST.future.Button
     *              asItem();        // now back to the item
     *
     * This method is *only* applicable in conjuction with Ext JS Modern Toolkit (or Sencha Touch) when
     * using an Ext.dataview.DataView that is configured with `useComponents:true`.
     *
     * @return {ST.future.Item}
     */
    asItem: function () {
        var item = this.getRelated('item');
        var chainItem = {
            locator: '$$SKIP$$',
            futureClsName: 'ST.future.Item',
            type: 'element'
        };
        // adding an extra locatorChain item since one won't get added by default
        item.locator.locatorChain.push(chainItem);
        // replay locator to ensure context is updated appropriately
        this.play([item.locator]);
 
        return item;
    },
 
    _attach: function () {
        this.cmp = this.el.getComponent();
    },
 
    _getFocusEl: function() {
        var cmp = this.cmp,
            el, fld;
 
        // if using classic toolkit, we can use getFocusEl()
        if (ST.isClassical()) {
            el = cmp.getFocusEl();
        }
        // if not classic and the type is a textfield, we can retrieve the input from the component
        else if (cmp.isXType('textfield')) {
            if (cmp.getComponent) {
                fld = cmp.getComponent();
            } else {
                // in 6.5, Ext.field.Text no longer extends Ext.field.Decorator, so getComponent() doesn't exist
                // however, the "field" component is just the component itself, so easy enough
                fld = cmp;
            }
            
            el = fld.input || fld.inputElement; // 6.2+ changed input to inputElement
        }
        // otherwise, just fallback to the element; this will accomodate Sencha Touch, and is the default for
        // what getFocusEl() returns in the modern toolkit
        else {
            el = cmp.el || cmp.element;
        }
 
        return el;
    }
});
 
/**
 * Returns a {@link ST.future.Component future component} used to queue operations for
 * when that component becomes available.
 * @param {String} locator See {@link ST.Locator} for supported syntax.
 * @param {Number} [timeout] The maximum time (in milliseconds) to wait for the component.
 * @return {ST.future.Component}
 * @method component
 * @member ST
 */
 
//-----------------------------------------------------------------------------
// Button
 
/**
 * This class provides methods specific to Ext JS Buttons (`Ext.button.Button`).
 *
 * ## Examples
 *
 *      // Clicking a button
 *      ST.button('#mybutton').
 *          click();
 * 
 *      // Checking a button is disabled
 *      ST.button('#mybutton').
 *          disabled();
 *
 * ## Futures
 * 
 * The general mechanics of futures is covered in {@link ST.future.Element}.
 * 
 * ## Locators
 * 
 * See {@link ST.Locator} for supported locator syntax.
 * 
 * @class ST.future.Button
 * @extend ST.future.Component
 */
ST.future.define('Button', {
    extend: ST.future.Component,
 
    //TODO menu, split
 
    playables: {
        /**
         * @method pressed
         * Waits for this button to be pressed.
         * This only applies to Ext JS / Classic and Modern toolkit toggle buttons (as of version 6.0.2) and not in Sencha Touch.
         *
         *      ST.button('@someButton').
         *          pressed().
         *          and(function (button) {
         *              // button is now pressed
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Button} this
         * @chainable
         */
        pressed: {
            is: function () {
                var cmp = this.getComponent();
                return ST.isClassic ? cmp.pressed : cmp.getPressed();
            },
            wait: ['toggle', 'pressedchange']
        },
 
        /**
         * @method unpressed
         * Waits for this button to be unpressed.
         * This only applies to Ext JS / Classic and Modern toolkit toggle buttons (as of version 6.0.2) and not in Sencha Touch.
         *
         *      ST.button('@someButton').
         *          unpressed().
         *          and(function (button) {
         *              // button is now unpressed
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Button} this
         * @chainable
         */
        unpressed: {
            is: function () {
                var cmp = this.getComponent();
                return ST.isClassic ? !cmp.pressed : !cmp.getPressed();
            },
            wait: ['toggle', 'pressedchange']
        },
 
        /**
         * @method expanded
         * Waits for this button's menu to be shown.
         * This only applies to Ext JS / Classic toolkit buttons and not in the Modern toolkit or Sencha Touch.
         *
         *      ST.button('@someButton').
         *          expanded().
         *          and(function (button) {
         *              // button's menu is now visible
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Button} this
         * @chainable
         */
        expanded: {
            is: function () {
                return this.getComponent().hasVisibleMenu();
            },
            wait: ['menushow']
        },
 
        /**
         * @method collapsed
         * Waits for this button's menu to be hidden.
         * This only applies to Ext JS / Classic toolkit buttons and not in the Modern toolkit or Sencha Touch.
         *
         *      ST.button('@someButton').
         *          collapsed().
         *          and(function (button) {
         *              // button's menu is now hidden
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Button} this
         * @chainable
         */
        collapsed: {
            is: function () {
                return !this.getComponent().hasVisibleMenu();
            },
            wait: ['menuhide']
        },
 
        /**
         * @method press
         * Schedules this button to be pressed.
         * This only applies to Ext JS / Classic and Modern toolkit toggle buttons (as of version 6.0.2) and not in Sencha Touch.
         *
         *      ST.button('@someButton').
         *          press().
         *          and(function (button) {
         *              // button has now been pressed.
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Button} this
         * @chainable
         */
        press: {
            params: 'timeout',
            fn: function () {
                this.getComponent().setPressed(true);
            }
        },
 
        /**
         * @method unpress
         * Schedules this button to be unpressed.
         * This only applies to Ext JS / Classic and Modern toolkit toggle buttons (as of version 6.0.2) and not in Sencha Touch.
         *
         *      ST.button('@someButton').
         *          unpress().
         *          and(function (button) {
         *              // button has now been unpressed.
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Button} this
         * @chainable
         */
        unpress: {
            params: 'timeout',
            fn: function () {
                this.getComponent().setPressed(false);
            }
        },
 
        /**
         * @method expand
         * Schedules this button's menu to be shown.
         * This only applies to Ext JS / Classic toolkit buttons and not in the Modern toolkit or Sencha Touch.
         *
         *      ST.button('@someButton').
         *          expand().
         *          and(function (button) {
         *              // button's menu is shown
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Button} this
         * @chainable
         */
        expand: {
            params: 'timeout',
            fn: function () {
                this.getComponent().showMenu();
            }
        },
 
        /**
         * @method collapse
         * Schedules this button's menu to be hidden.
         * This only applies to Ext JS / Classic toolkit buttons and not in the Modern toolkit or Sencha Touch.
         *
         *      ST.button('@someButton').
         *          collapse().
         *          and(function (button) {
         *              // button's menu is hidden
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Button} this
         * @chainable
         */
        collapse: {
            params: 'timeout',
            fn: function () {
                this.getComponent().hideMenu();
            }
        }
    }
});
 
/**
 * Returns a {@link ST.future.Button future button} used to queue operations for
 * when that button becomes available.
 * @param {String} locator See {@link ST.Locator} for supported syntax.
 * @param {Number} [timeout] The maximum time (in milliseconds) to wait for the button.
 * @return {ST.future.Button}
 * @method button
 * @member ST
 */
 
//-----------------------------------------------------------------------------
// Field
 
/**
 * This class provides methods specific to Ext JS Field (`Ext.form.field.Base`, `Ext.field.Field`). This
 * class can be used to wait on component states like so:
 *
 *      ST.field('@someField').
 *          value(42).
 *          and(function (field) {
 *              // field has received the value 42
 *          });
 *
 * It can also be used to manipulate the component state:
 *
 *      ST.field('@someField').
 *          setValue(42).
 *          and(function (field) {
 *              // field has now been set to the value 42
 *          });
 * 
 * ## Futures
 * 
 * The general mechanics of futures is covered in {@link ST.future.Element}.
 * 
 * ## Locators
 * 
 * See {@link ST.Locator} for supported locator syntax.
 * 
 * @class ST.future.Field
 * @extend ST.future.Component
 */
ST.future.define('Field', {
    extend: ST.future.Component,
 
    playables: {
        /**
         * @method value
         * Waits for this Field to have the specified value.
         *
         *      ST.field('@someField').
         *          value('Something').
         *          and(function (field) {
         *              // textField has received the value "Something"
         *          });
         *
         * @param {String} value The value for which to wait.
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Field} this
         * @chainable
         */
        value: {
            params: 'value',
            is: function () {
                var v = this.getComponent().getValue();
                return v === this.args.value;
            },
            wait: 'change'
        },
 
        /**
         * @method valueEmpty
         * Waits for this Field to have an empty value.
         *
         *      ST.field('@someField').
         *          valueEmpty().
         *          and(function (field) {
         *              // field value is now empty
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Field} this
         * @chainable
         */
        valueEmpty: {
            is: function () {
                var v = this.getComponent().getValue();
                return !(v || v === 0);
            },
            wait: 'change'
        },
 
        /**
         * @method valueNotEmpty
         * Waits for this Field to have a non-empty value.
         *
         *      ST.field('@someField').
         *          valueNotEmpty().
         *          and(function (field) {
         *              // field value is now non-empty
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Field} this
         * @chainable
         */
        valueNotEmpty: {
            is: function () {
                var v = this.getComponent().getValue();
                return (v || v === 0);
            },
            wait: 'change'
        },
 
        /**
         * @method setValue
         * @chainable
         * Schedules a `setValue` call on the underlying Field.
         *
         *      ST.field('@someField').
         *          setValue('Something').
         *          and(function (field) {
         *              // field value is now "Something"
         *          });
         *
         * @param {String} value The new value for the text field.
         * @param {Number/Object} [timeout] The maximum time (in milliseconds) to wait.
         * @returns {ST.future.Field}
         */
        setValue: {
            addEvent: function () {
                var me = this,
                    rec = me._buildRec('setValue', arguments, 'value,timeout', {});
 
                me.play([rec]);
 
                return me;
            },
            fn: function () {
                var cmp = this.getComponent(),
                    value = this.args.value;
 
                cmp.setValue(value);
            }
        }
    }
});
 
/**
 * Returns a {@link ST.future.Field future field} used to queue operations for
 * when that component becomes available.
 * @param {String} locator See {@link ST.Locator} for supported syntax.
 * @param {Number} [timeout] The maximum time (in milliseconds) to wait for the component.
 * @return {ST.future.Field}
 * @method field
 * @member ST
 */
 
//-----------------------------------------------------------------------------
// Checkbox
 
/**
 * This class provides methods specific to Ext JS Checkbox (`Ext.form.field.Checkbox` or `Ext.field.Checkbox`).
 * In addition to `ST.future.Field` features, this class adds value `checked` and
 * `unchecked` testing. For example:
 *
 *      ST.checkBox('@someCheckBox').
 *          checked().
 *          and(function (checkbox) {
 *              // now checked
 *          }).
 *          unchecked().
 *          and(function (checkbox) {
 *              // not unchecked
 *          });
 * 
 * ## Futures
 * 
 * The general mechanics of futures is covered in {@link ST.future.Element}.
 * 
 * ## Locators
 * 
 * See {@link ST.Locator} for supported locator syntax.
 * 
 * @class ST.future.CheckBox
 * @extend ST.future.Field
 */
ST.future.define('CheckBox', {
    extend: ST.future.Field,
    playables: {
        /**
         * @method value
         * Waits for this CheckBox to have the specified value.
         *
         *      ST.checkBox('@someCheckBox').
         *          value(true);
         *
         * NOTE: The `value` of the underlying checkbox component may differ from the `checked` state,
         * depending on the Ext JS toolkit being used (or Sencha Touch), which may produce unexpected results
         * when using `value()` on the CheckBox future.
         *
         * It is therefore recommended to use `checked()` and/or `unchecked()` when interacting with
         * the checked state of the underlying checkbox component.
         *
         * @param {String} value The value for which to wait.
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.CheckBox} this
         * @chainable
         */
 
        /**
         * @method valueEmpty
         * Waits for this CheckBox to have an empty value.
         *
         *      ST.checkBox('@someCheckBox').
         *          valueEmpty().
         *          and(function (checkBox) {
         *              // checkbox value is now empty
         *          });
         *
         * NOTE: The `value` of the underlying checkbox component may differ from the `checked` state,
         * depending on the Ext JS toolkit being used (or Sencha Touch), which may produce unexpected results
         * when using `valueEmpty()` on the CheckBox future.
         *
         * It is therefore recommended to use `checked()` and/or `unchecked()` when interacting with
         * the checked state of the underlying checkbox component.
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.CheckBox} this
         * @chainable
         */
 
        /**
         * @method valueNotEmpty
         * Waits for this CheckBox to have a non-empty value.
         *
         *      ST.checkBox('@someCheckBox').
         *          valueNotEmpty().
         *          and(function (checkBox) {
         *              // checkbox value is now non-empty
         *          });
         *
         *
         * NOTE: The `value` of the underlying checkbox component may differ from the `checked` state,
         * depending on the Ext JS toolkit being used (or Sencha Touch), which may produce unexpected results
         * when using `valueNotEmpty()` on the CheckBox future.
         *
         * It is therefore recommended to use `checked()` and/or `unchecked()` when interacting with
         * the checked state of the underlying checkbox component.
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.CheckBox} this
         * @chainable
         */
 
        /**
         * @method checked
         * Waits for the underlying CheckBox to be `checked`.
         *
         *      ST.checkBox('@someCheckBox').
         *          checked().
         *          and(function (checkBox) {
         *              // checkBox is in a checked state
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.CheckBox} this
         * @chainable
         */
        checked: {
            is: function () {
                var cmp = this.getComponent();
                return ST.isClassic ? cmp.checked : cmp.getChecked();
            },
            wait: 'change'
        },
 
        /**
         * @method unchecked
         * @chainable
         * Waits for the underlying CheckBox to be `unchecked`.
         *
         *      ST.checkBox('@someCheckBox').
         *          unchecked().
         *          and(function (checkBox) {
         *              // checkBox is in an unchecked state
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.CheckBox} this
         */
        unchecked: {
            is: function () {
                var cmp = this.getComponent();
                return ST.isClassic ? !cmp.checked : !cmp.getChecked();
            },
            wait: 'change'
        },
 
        /**
         * @method setValue
         * @chainable
         * Schedules a `setValue` call on the underlying CheckBox.
         *
         *      ST.checkBox('@someField').
         *          setValue(true);
         *
         * NOTE: The `value` of the underlying checkbox component may differ from the `checked` state,
         * depending on the Ext JS toolkit being used (or Sencha Touch), which may produce unexpected results
         * when using `setValue()` on the CheckBox future.
         *
         * It is therefore recommended to use `check()`, `uncheck()`, and `setChecked()`
         * when interacting with the checked state of the underlying checkbox component.
         *
         * @param {String} value The new value for the checkbox field.
         * @param {Number/Object} [timeout] The maximum time (in milliseconds) to wait.
         * @returns {ST.future.CheckBox}
         */
 
        /**
         * @method check
         * @chainable
         * Schedules the checkbox to be checked.
         *
         *      ST.checkBox('@someCheckBox').
         *          check().
         *          and(function (checkBox) {
         *              // checkBox is now checked
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @returns {ST.future.CheckBox}
         */
        check: {
            params: 'timeout',
            fn: function () {
                ST.future.CheckBox._setChecked(this.getComponent(), true);
            }
        },
 
        /**
         * @method uncheck
         * @chainable
         * Schedules the checkbox to be unchecked.
         *
         *      ST.checkBox('@someCheckBox').
         *          uncheck().
         *          and(function (checkBox) {
         *              // checkBox is now unchecked
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @returns {ST.future.CheckBox}
         */
        uncheck: {
            params: 'timeout',
            fn: function () {
                ST.future.CheckBox._setChecked(this.getComponent(), false);
            }
        }
    },
 
    statics: {
        _setChecked: function (cmp, checked) {
            if (ST.isClassic) {
                // classic does not have setChecked, so we need to use setValue
                cmp.setValue(checked);
            } else {
                cmp.setChecked(checked);
            }
        }
    }
});
 
/**
 * Returns a {@link ST.future.CheckBox future checkBox} used to queue operations for
 * when that component becomes available.
 * @param {String} locator See {@link ST.Locator} for supported syntax.
 * @param {Number} [timeout] The maximum time (in milliseconds) to wait for the component.
 * @return {ST.future.CheckBox}
 * @method checkBox
 * @member ST
 */
 
//-----------------------------------------------------------------------------
// Slider
 
/**
 * This class provides methods specific to Ext JS Slider (`Ext.Slider` or `Ext.field.Slider`).
 *
 * ## Examples
 *
 *      // Set the value of the Slider
 *      ST.slider('#myslider').
 *          setValue(23);
 *  
 *      // Waits for the Slider to have the specified value
 *      ST.slider('#myslider').
 *          value(23);
 * 
 * ## Futures
 * 
 * The general mechanics of futures is covered in {@link ST.future.Element}.
 * 
 * ## Locators
 * 
 * See {@link ST.Locator} for supported locator syntax.
 * 
 * @class ST.future.Slider
 * @extend ST.future.Field
 */
ST.future.define('Slider', {
    extend: ST.future.Field,
    playables: {
        /**
         * @method value
         * Waits for this Slider to have the specified value.
         *
         *      ST.slider('@someCSlider').
         *          value(20);
         *
         *      ST.slider('@multiSlider)
         *          value([25, 65, 95])
         *
         * NOTE: When asserting the value with `value()`, or setting the value with `setValue()`,
         * you can supply either a numeric value (20) or an array with a single numeric value ([20])
         * for 'single' sliders using the Classic toolkit, or a slider with a single thumb in the Modern toolkit.
         *
         * If using a multi-thumb slider in either toolkit, you should always provide an array of values when asserting or setting the value.
         *
         * @param {String/String[]} value The value(s) for which to wait.
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Slider} this
         * @chainable
         */
        value: {
            params: 'value',
            is: function () {
                var v = this.getComponent().getValue(),
                    argV = this.args.value,
                    finalValue = ST.isArray(v) ? v : [v],
                    finalArgs = ST.isArray(argV) ? argV : [argV];
 
                return ST.Array.equals(finalValue, finalArgs);
            },
            wait: 'change'
        }
    }
});
 
/**
 * Returns a {@link ST.future.Slider future slider} used to queue operations for
 * when that component becomes available.
 * @param {String} locator See {@link ST.Locator} for supported syntax.
 * @param {Number} [timeout] The maximum time (in milliseconds) to wait for the component.
 * @return {ST.future.Slider}
 * @method slider
 * @member ST
 */
 
//-----------------------------------------------------------------------------
// TextField
 
/**
 * This class provides methods specific to Ext JS TextField (`Ext.form.field.Text` or `Ext.field.Text`).
 * In addition to `ST.future.Field` features, this class adds value similarity
 * checking. For example:
 *
 *      ST.textField('@someTextField').
 *          valueLike('world').
 *          and(function (textField) {
 *              // the value now contains the text "world"
 *          }).
 *          valueNotLike(/^hello/i).
 *          and(function (textField) {
 *              // the value no longer starts with "hello" (ignoring case)
 *          });
 *
 * ## Futures
 * 
 * The general mechanics of futures is covered in {@link ST.future.Element}.
 * 
 * ## Locators
 * 
 * See {@link ST.Locator} for supported locator syntax.
 *  
 * @class ST.future.TextField
 * @extend ST.future.Field
 */
ST.future.define('TextField', {
    extend: ST.future.Field,
 
    playables: {
        /**
         * @method valueLike
         * Waits for this TextField to have a value like the given `pattern`.
         *
         *      ST.textField('@someTextField').
         *          valueLike(/bar$/i).
         *          and(function (textField) {
         *              // textField value now ends with "bar" (ignoring case)
         *          });
         *
         * @param {RegExp/String} pattern The pattern to match. If this is a String,
         * it is first promoted to a `RegExp` by called `new RegExp(pattern)`.
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.TextField} this
         * @chainable
         */
        valueLike: {
            addEvent: function () {
                var me = this,
                    rec = me._buildRec('valueLike', arguments, 'pattern,timeout', {
                        waitingFor: 'value',
                        waitingState: 'like '
                    });
 
                rec.waitingState += rec.args.pattern.source || rec.args.pattern.toString();
 
                me.play([rec]);
 
                return me;
            },
            is: function () {
                var cmp = this.getComponent(),
                    v = cmp.getValue(),
                    pattern = ST.decodeRegex(this.args.pattern),
                    re = (typeof pattern === 'string') ? new RegExp(pattern) : pattern;
 
                return re.test(v);
            },
            wait: 'change'
        },
 
        /**
         * @method valueNotLike
         * Waits for this TextField to have a value that does not match the given `pattern`.
         *
         *      ST.textField('@someTextField').
         *          valueNotLike(/bar$/i).
         *          and(function (textField) {
         *              // textField value does not end with "bar" (ignoring case)
         *          });
         *
         * @param {RegExp/String} pattern The pattern to match. If this is a String,
         * it is first promoted to a `RegExp` by called `new RegExp(pattern)`.
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.TextField} this
         * @chainable
         */
        valueNotLike: {
            addEvent: function () {
                var me = this,
                    rec = me._buildRec('valueNotLike', arguments, 'pattern,timeout', {
                        waitingFor: 'value',
                        waitingState: 'not like '
                    });
 
                rec.waitingState += rec.args.pattern.source || rec.args.pattern.toString();
 
                me.play([rec]);
 
                return me;
            },
            is: function () {
                var cmp = this.getComponent(),
                    v = cmp.getValue(),
                    pattern = ST.decodeRegex(this.args.pattern),
                    re = (typeof pattern === 'string') ? new RegExp(pattern) : pattern;
 
                return !re.test(v);
            },
            wait: 'change'
        }
    }
});
 
/**
 * Returns a {@link ST.future.TextField future textfield} used to queue operations for
 * when that component becomes available.
 * @param {String} locator See {@link ST.Locator} for supported syntax.
 * @param {Number} [timeout] The maximum time (in milliseconds) to wait for the component.
 * @return {ST.future.TextField}
 * @method textField
 * @member ST
 */
 
//-----------------------------------------------------------------------------
// Picker
 
/**
 * This class provides methods specific to Ext JS Pickers (`Ext.form.field.Picker`). This only applies to Ext JS / Classic
 * and not in the Modern toolkit (as of version 6.0.1) or Sencha Touch.
 *
 * @class ST.future.Picker
 * @extend ST.future.TextField
 * @private
 */
ST.future.define('Picker', {
    extend: ST.future.TextField,
 
    playables: {
        /**
         * @method collapsed
         * Waits for this picker to be collapsed.
         *
         *      ST.picker('@somePicker').
         *          collapsed().
         *          and(function (picker) {
         *              // picker is now collapsed
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Picker} this
         * @chainable
         */
        collapsed: {
            is: function () {
                return !this.getComponent().isExpanded;
            },
            wait: 'collapse'
        },
 
        /**
         * @method expanded
         * Waits for this picker to be expanded.
         *
         *      ST.picker('@somePicker').
         *          expanded().
         *          and(function (picker) {
         *              // picker is now expanded
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Picker} this
         * @chainable
         */
        expanded: {
            is: function () {
                return this.getComponent().isExpanded;
            },
            wait: 'expand'
        },
 
        /**
         * @method collapse
         * Schedules this picker to collapse. All arguments passed to this method will be
         * forwarded to the `collapse` method of the picker at the appropriate time so
         * consult the documentation for the actual framework and version you are using
         * for parameter details.
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Picker} this
         * @chainable
         */
        collapse: {
            params: 'timeout',
            fn: function () {
                this.getComponent().collapse();
            }
        },
 
        /**
         * @method expand
         * Schedules this picker to expand. All arguments passed to this method will be
         * forwarded to the `expand` method of the picker at the appropriate time so
         * consult the documentation for the actual framework and version you are using
         * for parameter details.
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Picker} this
         * @chainable
         */
        expand: {
            params: 'timeout',
            fn: function () {
                this.getComponent().expand();
            }
        }
    }
});
 
/**
 * Returns a {@link ST.future.Picker future picker} used to queue operations for
 * when that `Ext.form.field.Picker` becomes available.
 * @param {String} locator See {@link ST.Locator} for supported syntax.
 * @param {Number} [timeout] The maximum time (in milliseconds) to wait for the picker.
 * @return {ST.future.Picker}
 * @method picker
 * @member ST
 */
 
//-----------------------------------------------------------------------------
// ComboBox
 
/**
 * This class provides methods specific to Ext JS ComboBox (`Ext.form.field.ComboBox`).
 * This is only available in Ext JS / Classic, not in the Modern toolkit (as of version 6.0.1) or Sencha Touch.
 * 
 * ## Examples
 *
 *      // Expand a ComboBox and locate a list item by its display value
 *      ST.comboBox('#mycombo').
 *          expand().
 *          down("//li[text()='District of Columbia']").
 *          click();
 *
 *      // Expand a ComboBox and locate a list item by its record index
 *      ST.comboBox('#mycombo').
 *          expand().
 *          down('//li[@data-recordindex="2"]').
 *          click();
 *  
 * ## Futures
 * 
 * The general mechanics of futures is covered in {@link ST.future.Element}.
 * 
 * ## Locators
 * 
 * See {@link ST.Locator} for supported locator syntax.
 * 
 * @class ST.future.ComboBox
 * @extend ST.future.Picker
 */
ST.future.define('ComboBox', {
    extend: ST.future.Picker,
 
    playables: {
        // TODO - queried
        // TODO - loaded
        // TODO - selected
        /**
         * @method displayValue
         * Waits for this ComboBox to have the specified display value.
         *
         *      ST.comboBox('@someCombo')
         *          .displayValue('Something');
         *
         * @param {String} value The display value for which to wait.
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.ComboBox} this
         * @chainable
         */
        displayValue: {
            params: 'value',
            is: function () {
                var me = this,
                    value = me.args.value,
                    cmp = me.getComponent(),
                    record = ST.future.ComboBox.findRecordByDisplay(cmp, value),
                    v;
 
                if (record) {
                    v = record.get(cmp.getDisplayField ? cmp.getDisplayField() : cmp.displayField);
                }
 
                return v === value;
            },
            wait: 'change'
        }
    },
 
    statics: {
        /**
         * @private
         */
        findRecordByDisplay: function (cmp, value) {
            if (ST.isClassic || ST.sdkVersion.gtEq('6.5.1')) {
                return cmp.findRecordByDisplay(value);
            } else {
                // in modern 6.5.0, there is a bad bug...fix it here
                var store = cmp.getStore(),
                    result,
                    ret = false;
 
                if (store) {
                    result = store.byText.get(value);
                    
                    if (result) {
                        ret = result[0] || result;
                    }
                }
                return ret;
            }
        }
    }
});
 
/**
 * Returns a {@link ST.future.ComboBox future ComboBox} used to queue operations for
 * when that `Ext.form.field.ComboBox` becomes available.
 * @param {String} locator See {@link ST.Locator} for supported syntax.
 * @param {Number} [timeout] The maximum time (in milliseconds) to wait for the combobox.
 * @return {ST.future.ComboBox}
 * @method comboBox
 * @member ST
 */
 
//-----------------------------------------------------------------------------
// Select
 
/**
 * This class provides methods specific to Ext JS Select fields (`Ext.field.Select`).
 * This only applies to Ext JS / Modern and Sencha Touch, and is not available in the Classic toolkit.
 *
 * @class ST.future.Select
 * @extend ST.future.TextField
 */
ST.future.define('Select', {
    extend: ST.future.TextField,
 
    playables: {
        /**
         * @method collapsed
         * Waits for this select field to be collapsed.
         *
         *      ST.select('@someSelect').
         *          collapsed().
         *          and(function (select) {
         *              // select field is now collapsed
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Select} this
         * @chainable
         */
        collapsed: {
            is: function () {
                var picker = ST.future.Select.getPicker(this.getComponent());
                return picker.isHidden();
            },
            wait: function (done) {
                var picker = ST.future.Select.getPicker(this.getComponent()),
                    listener = {
                        hide: done,
                        single: true
                    };
 
                picker.on(listener);
 
                return function () {
                    picker.un(listener)
                }
            }
        },
 
        /**
         * @method expanded
         * Waits for this select field to be expanded.
         *
         *      ST.select('@someSelect').
         *          expanded().
         *          and(function (select) {
         *              // select field is now expanded
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Select} this
         * @chainable
         */
        expanded: {
            is: function () {
                var picker = ST.future.Select.getPicker(this.getComponent())
                return !picker.isHidden() && picker.isPainted();
            },
            wait: function (done) {
                var picker = ST.future.Select.getPicker(this.getComponent()),
                    listener = {
                        show: done,
                        single: true
                    };
 
                picker.on(listener);
 
                return function () {
                    picker.un(listener)
                }
            }
        },
 
        /**
         * @method collapse
         * Schedules this select field to collapse. All arguments passed to this method will be
         * forwarded to the `collapse` method of the select field at the appropriate time so
         * consult the documentation for the actual framework and version you are using
         * for parameter details.
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Select} this
         * @chainable
         */
        collapse: {
            params: 'timeout',
            fn: function (done) {
                var cmp = this.getComponent(),
                    picker = ST.future.Select.getPicker(cmp),
                    needsCallback = (typeof cmp.getUsePicker !== 'undefined') ? cmp.getUsePicker() : cmp.getPicker();
 
                if (needsCallback) {
                    picker.on('hide', function () {
                        done();
                    }, {
                        single: true
                    });
                    picker.hide();
                } else {
                    picker.hide();
                    done();
                }
            }
        },
 
        /**
         * @method expand
         * Schedules this select field to expand. All arguments passed to this method will be
         * forwarded to the `expand` method of the select field at the appropriate time so
         * consult the documentation for the actual framework and version you are using
         * for parameter details.
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Select} this
         * @chainable
         */
        expand: {
            params: 'timeout',
            fn: function (done) {
                var cmp = this.getComponent(),
                    picker = ST.future.Select.getPicker(cmp),
                    needsCallback = (typeof cmp.getUsePicker !== 'undefined') ? cmp.getUsePicker() : cmp.getPicker();
 
                if (needsCallback) {
                    picker.on('show', function () {
                        done();
                    }, {
                        single: true
                    });
                    cmp.showPicker();
                } else {
                    cmp.showPicker();
                    done();
                }
            }
        }
    },
 
    statics: {
        /**
         * @private
         */
        getPicker: function (component) {
            if (typeof component.getUsePicker !== 'undefined' || typeof component.getTabletPicker !== 'undefined') {
                return component.getUsePicker() ? component.getPhonePicker() : component.getTabletPicker();
            } 
            return component.getPicker();
        }
    }
});
 
/**
 * Returns a {@link ST.future.Select future select field} used to queue operations for
 * when that `Ext.field.Select` becomes available.
 * @param {String} locator See {@link ST.Locator} for supported syntax.
 * @param {Number} [timeout] The maximum time (in milliseconds) to wait for the select field.
 * @return {ST.future.Select}
 * @method select
 * @member ST
 */
 
//-----------------------------------------------------------------------------
// Panel
 
/**
 * This class provides methods specific to Ext JS Panels (`Ext.panel.Panel` or
 * `Ext.Panel`).
 * 
 * ## Examples
 *
 *      // Wait for a Panel to be visible
 *      ST.panel('#mypanel').
 *          visible().
 *          and(function (panel) {
 *              // panel is now visible
 *          });
 *
 *      // Locate a button within a panel and schedule a click on the button
 *      ST.panel('#mypanel').
 *          gotoButton('#mybutton').
 *          click();
 *  
 * ## Futures
 * 
 * The general mechanics of futures is covered in {@link ST.future.Element}.
 * 
 * ## Locators
 * 
 * See {@link ST.Locator} for supported locator syntax.
 * 
 * @class ST.future.Panel
 * @extend ST.future.Component
 */
ST.future.define('Panel', {
    extend: ST.future.Component,
 
    playables: {
        /**
         * @method collapsed
         * Waits for this panel to be collapsed. This only applies to Ext JS / Classic
         * panels and not in the Modern toolkit (as of version 6.0.1) or Sencha Touch.
         *
         *      ST.panel('@somePanel').
         *          collapsed().
         *          and(function (panel) {
         *              // panel is now collapsed
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Panel} this
         * @chainable
         */
        collapsed: {
            is: function () {
                return this.getComponent().collapsed;
            },
            wait: 'collapse'
        },
 
        /**
         * @method expanded
         * Waits for this panel to be expanded. This only applies to Ext JS / Classic
         * panels and not in the Modern toolkit (as of version 6.0.1) or Sencha Touch.
         *
         *      ST.panel('@somePanel').
         *          expanded().
         *          and(function (panel) {
         *              // panel is now expanded
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Panel} this
         * @chainable
         */
        expanded: {
            is: function () {
                return !this.getComponent().collapsed;
            },
            wait: 'expand'
        },
 
        /**
         * @method expand
         * Schedules this panel to expand. All arguments passed to this method will be
         * forwarded to the `expand` method of the panel at the appropriate time so
         * consult the documentation for the actual framework and version you are using
         * for parameter details.
         *
         * @param {Boolean} [animate] True to animate the transition, else false
         * (defaults to the value of the `animCollapse` panel config).  May
         * also be specified as the animation duration in milliseconds.
         * @return {ST.future.Panel} this
         * @chainable
         */
        expand: {
            addEvent: function () {
                var me = this,
                    rec = me._buildRec('expand',arguments, 'direction,animate,timeout', {});
 
                if ('animate' in rec.args) {
                    rec.args[0] = rec.args.animate;
                    delete rec.args.animate;
                }
 
                me.play([rec]);
 
                return me;
            },
 
            fn: function () {
                var cmp = this.getComponent();
 
                cmp.expand.apply(cmp, this.args);
            }
        },
 
        /**
         * @method collapse
         * Schedules this panel to collapse. All arguments passed to this method will be
         * forwarded to the `collapse` method of the panel at the appropriate time so
         * consult the documentation for the actual framework and version you are using
         * for parameter details.
         *
         * @param {String} [direction] The direction to collapse towards. Must be one of
         *
         *   - Ext.Component.DIRECTION_TOP
         *   - Ext.Component.DIRECTION_RIGHT
         *   - Ext.Component.DIRECTION_BOTTOM
         *   - Ext.Component.DIRECTION_LEFT
         *
         * Defaults to the panel's `collapseDirection` config.
         *
         * @param {Boolean} [animate] True to animate the transition, else false
         * (defaults to the value of the `animCollapse` panel config). May also be specified
         * as the animation duration in milliseconds.
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Panel} this
         * @chainable
         */
        collapse: {
            addEvent: function () {
                var me = this,
                    rec = me._buildRec('collapse',arguments, 'direction,animate,timeout', {});
 
                if ('direction' in rec.args) {
                    rec.args[0] = rec.args.direction;
                    delete rec.args.direction;
 
                    if ('animate' in rec.args) {
                        rec.args[1] = rec.args.animate;
                        delete rec.args.animate;
                    }
                }
 
                me.play([rec]);
 
                return me;
            },
            fn: function () {
                var cmp = this.getComponent();
 
                cmp.collapse.apply(cmp, this.args);
            }
        }
    }
});
 
/**
 * Returns a {@link ST.future.Panel future panel} used to queue operations for
 * when that panel becomes available.
 * @param {String} locator See {@link ST.Locator} for supported syntax.
 * @param {Number} [timeout] The maximum time (in milliseconds) to wait for the panel.
 * @return {ST.future.Panel}
 * @method panel
 * @member ST
 */
 
/**
 * @private
 * @class ST.future.SelectionModel
 * Mixin to provide selection-related capabilities to grid and dataviews. 
 */
ST.future.define('SelectionModel', {
    factoryable: false,
    valueProperty: false,
    playables: {
        /**
         * @method selected
         * Waits for the given records (by id) to be selected
         *
         *      ST.dataView('@someDataView').
         *          selected([1, 3]).
         *          and(function (dataView) {
         *              // 2 records are now selected
         *          });
         *
         * @param {Number/Number[]} id The ids of the records to select.
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @chainable
         */
        selected: {
            params: 'id',
            is: function () {
                var id = this.args.id,
                    config = {
                        ids: ST.isArray(id) ? id : [id],
                        type: 'id',
                        mode: 'select'
                    };
 
                return ST.future.SelectionModel._validateSelections(this.getComponent(), config);
            },
            wait: ['select']
        },
 
        /**
         * @method selectedAt
         * Waits for the given records (by index) to be selected
         *
         *      ST.dataView('@someDataView').
         *          selectedAt([1, 3]).
         *          and(function (dataView) {
         *              // 2 records are now selected
         *          });
         *
         * @param {Number/Number[]} index The indexes of the records to select.
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @chainable
         */
        selectedAt: {
            params: 'index',
            is: function (index) {
                var index = this.args.index,
                    config = {
                        indexes: ST.isArray(index) ? index : [index],
                        type: 'index',
                        mode: 'select'
                    };
 
                return ST.future.SelectionModel._validateSelections(this.getComponent(), config);
            },
            wait: ['select']
        },
 
        /**
         * @method selectedRange
         * Waits for the given records (by range of indexes) to be selected
         *
         *      ST.dataView('@someDataView').
         *          selectedRange(15, 45).
         *          and(function (dataView) {
         *              // range of records are now selected
         *          });
         *
         * @param {Number} start The starting index of the records to select.
         * @param {Number} [end] The ending index of the records to select. 
         * If not specified, the remainder of the available records will be selected
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @chainable
         */
        selectedRange: {
            params: 'start,end',
            is: function (start, end) {
                var config = {
                    start: this.args.start,
                    end: this.args.end,
                    type: 'range',
                    mode: 'select'
                };
 
                return ST.future.SelectionModel._validateSelections(this.getComponent(), config);
            },
            wait: ['select']
        },
 
        /**
         * @method selectedWith
         * Waits for the given records (by simple query) to be selected
         *
         *      ST.dataView('@someDataView').
         *          selectedWith('name', 'Doug').
         *          and(function (dataView) {
         *              // matching records are now selected
         *          });
         *
         * @param {String} propertyName The name of the property in the record against which to query.
         * @param {String} propertyValue The value against which to query.
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @chainable
         */
        selectedWith: {
            params: 'propertyName,propertyValue',
            is: function (propertyName, propertyValue) {
                var config = {
                    propertyName: this.args.propertyName,
                    propertyValue: this.args.propertyValue,
                    type: 'query',
                    mode: 'select'
                };
 
                return ST.future.SelectionModel._validateSelections(this.getComponent(), config);
            },
            wait: ['select']
        },
 
        /**
         * @method deselected
         * Waits for the given records (by id) to be deselected
         *
         *      ST.dataView('@someDataView').
         *          ... select records ...
         *          deselected([1, 3]).
         *          and(function (dataView) {
         *              // 2 records are now deselected
         *          });
         *
         * @param {Number/Number[]} id The ids of the records to deselect.
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @chainable
         */
        deselected: {
            params: 'id',
            is: function (id) {
                var id = this.args.id,
                    config = {
                        ids: ST.isArray(id) ? id : [id],
                        type: 'id',
                        mode: 'deselect'
                    };
 
                return ST.future.SelectionModel._validateSelections(this.getComponent(), config);
            },
            wait: ['deselect']
        },
 
        /**
         * @method deselectedAt
         * Waits for the given records (by index) to be deselected
         *
         *      ST.dataView('@someDataView').
         *          ... select records ...
         *          deselectedAt([1, 3]).
         *          and(function (dataView) {
         *              // 2 records are now deselected
         *          });
         *
         * @param {Number/Number[]} index The indexes of the records to deselect.
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @chainable
         */
        deselectedAt: {
            params: 'index',
            is: function (index) {
                var index = this.args.index,
                    config = {
                        indexes: ST.isArray(index) ? index : [index],
                        type: 'index',
                        mode: 'deselect'
                    };
 
                return ST.future.SelectionModel._validateSelections(this.getComponent(), config);
            },
            wait: ['deselect']
        },
 
        /**
         * @method deselectedRange
         * Waits for the given records (by range of indexes) to be deselected
         *
         *      ST.dataView('@someDataView').
         *          ... select records ...
         *          deselectedRange(15, 45).
         *          and(function (dataView) {
         *              // range of records are now deselected
         *          });
         *
         * @param {Number} start The starting index of the records to deselect.
         * @param {Number} [end] The ending index of the records to deselect. 
         * If not specified, the remainder of the available records will be deselected
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @chainable
         */
        deselectedRange: {
            params: 'start,end',
            is: function (start, end) {
                var config = {
                    start: this.args.start,
                    end: this.args.end,
                    type: 'range',
                    mode: 'deselect'
                };
 
                return ST.future.SelectionModel._validateSelections(this.getComponent(), config);
            },
            wait: ['deselect']
        },
 
        /**
         * @method deselectedWith
         * Waits for the given records (by simple query) to be deselected
         *
         *      ST.dataView('@someDataView').
         *          ... select records ...
         *          deselectedWith('name', 'Doug').
         *          and(function (dataView) {
         *              // matching records are now deselected
         *          });
         *
         * @param {String} propertyName The name of the property in the record against which to query.
         * @param {String} propertyValue The value against which to query.
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @chainable
         */
        deselectedWith: {
            params: 'propertyName,propertyValue',
            is: function (propertyName, propertyValue) {
                var config = {
                    propertyName: this.args.propertyName,
                    propertyValue: this.args.propertyValue,
                    type: 'query',
                    mode: 'deselect'
                };
 
                return ST.future.SelectionModel._validateSelections(this.getComponent(), config);
            },
            wait: ['deselect']
        },
 
        /**
         * @private
         * When the target is ready, this will execute the appropriate selection method based on the passed configuration
         * @param {Object} config The configuration object that will provide the necessary information to influence the selection
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         */
        _doSelect: {
            addEvent: function (config, timeout) {
                var me = this,
                    rec = me._buildRec('_doSelect', arguments, 'config,timeout', {
                        futureData: {
                            select: config,
                            scoped: true
                        }
                    });
 
                me.play([rec]);
            },
            fn: function () {
                var cmp = this.getComponent(),
                    opts = this.args.config,
                    records = ST.future.SelectionModel._getRecords(cmp, opts),
                    selModel;
 
                if (ST.isClassic) {
                    selModel = cmp.getSelectionModel();
                } else if (ST.sdkVersion.gtEq('6.5.0')) {
                    selModel = cmp.getSelectable();
                } else {
                    selModel = cmp;
                }
 
                if (opts.mode === 'select') {
                    selModel.select(records, opts.keepExisting);
                } else {
                    selModel.deselect(records);
                }
 
            }
        }
    },
 
    /**
     * Selects the requested record(s) given the record's `idProperty`.
     * @param {String/String[]} id The `idProperty` of the record(s) to select.
     * @param {Boolean} [keepExisting] `true` to preserve existing selections (default: `false`)
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @chainable
     */
    select: function (id, keepExisting, timeout) {
        return this.selectBy({
            ids: ST.isArray(id) ? id : [id],
            keepExisting: keepExisting,
            type: 'id'
        }, timeout);
    },
 
    /**
     * Selects the requested record(s) by index.
     * @param {String/String[]} index The index of the record(s) to select.
     * @param {Boolean} [keepExisting] `true` to preserve existing selections (default: `false`)
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @chainable
     */
    selectAt: function (index, keepExisting, timeout) {
        return this.selectBy({
            indexes: ST.isArray(index) ? index : [index],
            keepExisting: keepExisting,
            type: 'index'
        }, timeout);
    },
 
    /**
     * Selects the requested record(s) by index range.
     * @param {Number} start The starting index for the selection.
     * @param {Number} [end] The ending index for the selection. 
     * If not specified, the full range from the starting index will be included
     * @param {Boolean} [keepExisting] `true` to preserve existing selections (default: `false`)
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @chainable
     */
    selectRange: function (start, end, keepExisting, timeout) {
        return this.selectBy({
            start: start,
            end: end,
            keepExisting: keepExisting,
            type: 'range'
        }, timeout);
    },
 
    /**
     * Selects the requested record(s) by a simple property/value query.
     * @param {String} propertyName The name of the property by which to query.
     * @param {String} propertyValue The value by which to query.
     * @param {Boolean} [keepExisting] `true` to preserve existing selections (default: `false`)
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @chainable
     */
    selectWith: function (propertyName, propertyValue, keepExisting, timeout) {
        return this.selectBy({
            propertyName: propertyName,
            propertyValue: propertyValue,
            keepExisting: keepExisting,
            type: 'query'
        }, timeout);
    },
 
    /**
     * Selects all available records
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @chainable
     */
    selectAll: function (timeout) {
        return this.selectBy({
            type: 'all'
        }, timeout);
    },
 
    /**
     * Deselects the requested record(s) given the record's `idProperty`.
     * @param {String/String[]} id The `idProperty` of the record(s) to deselect.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @chainable
     */
    deselect: function (id, timeout) {
        return this.deselectBy({
            ids: ST.isArray(id) ? id : [id],
            type: 'id'
        }, timeout);
    },
 
    /**
     * Deselects the requested record(s) by index.
     * @param {String/String[]} index The index of the record(s) to deselect.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @chainable
     */
    deselectAt: function (index, timeout) {
        return this.deselectBy({
            indexes: ST.isArray(index) ? index : [index],
            type: 'index'
        }, timeout);
    },
 
    /**
     * Deselects the requested record(s) by index range.
     * @param {Number} start The starting index for the deselection.
     * @param {Number} [end] The ending index for the deselection. 
     * If not specified, the full range from the starting index will be included.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @chainable
     */
    deselectRange: function (start, end, timeout) {
        return this.deselectBy({
            start: start,
            end: end,
            type: 'range'
        }, timeout);
    },
 
    /**
     * Deselects the requested record(s) by a simple property/value query.
     * @param {String} propertyName The name of the property by which to query.
     * @param {String} propertyValue The value by which to query.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @chainable
     */
    deselectWith: function (propertyName, propertyValue, timeout) {
        return this.deselectBy({
            propertyName: propertyName,
            propertyValue: propertyValue,
            type: 'query'
        }, timeout);
    },
 
    /**
     * Deselects all selected records
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @chainable
     */
    deselectAll: function (timeout) {
        return this.deselectBy({
            type: 'all'
        }, timeout);
    },
 
    /**
     * Selects records given a config object that specified the match criteria.
     * @param {Object} config Configuration options for the selection.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @chainable
     */
    selectBy: function (config, timeout) {
        config.mode = 'select';
 
        this._doSelect(config, timeout);
 
        return this;
    },
 
    /**
     * Deselects records given a config object that specified the match criteria.
     * @param {Object} config Configuration options for the deselection.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @chainable
     */
    deselectBy: function (config, timeout) {
        config.mode = 'deselect';
 
        this._doSelect(config, timeout);
 
        return this;
    },
 
    statics: {
        /**
         * @private
         * Validates current selections against the passed configuration options
         * @param {Ext.Component} cmp The Ext JS component on which to perform the action
         * @param {Object} config Configuration for validation of selections
         * @return {Boolean}
         */
        _validateSelections: function (cmp, config) {
            var selections = ST.isClassic ? cmp.getSelectionModel().getSelection() : cmp.getSelections(),
                records = ST.future.SelectionModel._getRecords(cmp, config),
                expectedLength = (config.ids && config.ids.length) || (config.indexes && config.indexes.length),
                matchCount = 0,
                useLength = config.mode === 'select' ? true : false,
                len = records.length,
                i;
 
            // if we have an expected number of records (id, index searches), but the length of those
            // is not the same as the number of found records, this is a failure and we can abort
            if (ST.isNumber(expectedLength) && expectedLength !== len) {
                return false;
            }
 
            for (i=0; i<len; i++) {
                if (selections.indexOf(records[i]) !== -1) {
                    matchCount++;
                }
            }
 
            return matchCount === (useLength ? len : 0);        
        },
 
        /**
         * @private
         * Utility to retrieve collection of records based on various criteria
         * @param {Ext.Component} cmp The Ext JS component on which to perform the action
         * @param {Object} options Configuration options to influence how records are retrieved
         * @return {Array}
         */
        _getRecords: function (cmp, options) {
            var store = cmp.getStore(),
                type = options.type,
                ids = options.ids,
                indexes = options.indexes,
                start = options.start,
                end = options.end,
                propertyName = options.propertyName,
                propertyValue = options.propertyValue,
                records = [],
                fn, items, len, value, record, i;
 
            switch (type) {
                case 'all': 
                    records = store.getRange();
                    break;
                case 'id':
                case 'index':
                    fn = type === 'id' ? 'getById' : 'getAt';
                    items = type === 'id' ? ids : indexes;
                    len = items.length;
                    // for id/index based searches, loop over options and use appropriate
                    // method in store to retrieve record
                    for (i=0; i<len; i++) {
                        record = store[fn](items[i]);
 
                        if (record) {
                            records.push(record);
                        }
                    }
 
                    break;
                case 'range': 
                    records = store.getRange(start, end); 
                    break;
                case 'query':
                    store.each(function (record) {
                        value = record.get(propertyName);
 
                        if (value !== undefined && value === propertyValue) {
                            records.push(record);
                        }
                    });
                    break;
            }
            
            return records;
        }
    }
});
 
/**
 * @private
 * @class ST.future.Selection
 * Mixin to provide selection-related capabilities to rows and data view items. 
 */
ST.future.define('Selection', {
    factoryable: false,
    valueProperty: false,
    playables: {
        /**
         * @method selected
         * Waits for the record to be selected
         *
         *      ST.dataView('@someDataView').
         *          itemAt(1).
         *          selected().
         *          and(function (item) {
         *              // item is now selected
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @chainable
         */
        selected: {
            is: function () {
                var data = this.getFutureData(),
                    cmp = this.future.getRelated('dataView').cmp,
                    config = {
                        indexes: [data.recordIndex],
                        type: 'index',
                        mode: 'select'
                    };
 
                return ST.future.SelectionModel._validateSelections(cmp, config);
            },
            wait: function (done) {
                var me = this,
                    data = me.getFutureData(),
                    cmp = this.future.related.dataView.cmp,
                    listener = {
                        select: function (view, record) {
                            var selections = ST.isClassic ? view.getSelection() : view.getSelections(),
                                store = view.getStore(),
                                record = store.getAt(data.recordIndex);
 
                            if (ST.Array.indexOf(selections, record) !== -1) {
                                cmp.un(listener);
                                done();
                            }
                        }
                    };                 
 
                cmp.on(listener);
 
                return function () {
                    cmp.un(listener)
                }
            }
        },
 
        /**
         * @method deselected
         * Waits for the record to be deselected
         *
         *      ST.dataView('@someDataView').
         *          itemAt(150).
         *          selected().
         *          reveal().
         *          deselected().
         *          and(function (item) {
         *              // item is now deselected
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @chainable
         */
        deselected: {
            is: function () {
                var data = this.getFutureData(),
                    cmp = this.future.getRelated('dataView').cmp,
                    config = {
                        indexes: [data.recordIndex],
                        type: 'index',
                        mode: 'deselect'
                    };
 
                return ST.future.SelectionModel._validateSelections(cmp, config);
            },
            wait: function (done) {
                var me = this,
                    data = me.getFutureData(),
                    cmp = this.future.related.dataView.cmp,
                    listener = {
                        deselect: function (view, record) {
                            var selections = ST.isClassic ? view.getSelection() : view.getSelections(),
                                store = view.getStore(),
                                record = store.getAt(data.recordIndex);
 
                            if (ST.Array.indexOf(selections, record) === -1) {
                                cmp.un(listener);
                                done();
                            }
                        }
                    };                 
 
                cmp.on(listener);
 
                return function () {
                    cmp.un(listener)
                }
            }
        },
 
        /**
         * @method select
         * @chainable
         * Selects the item's record
         * @param {Boolean} [keepExisting] `true` to preserve existing selections (default: `false`)
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         */
        select: {
            addEvent: function (keepExisting, timeout) {
                var me = this;
 
                me.play(me._buildRec('select',{
                    timeout: timeout,
                    instanceData: {
                        keepExisting: true
                    }
                }));
 
                return me;
            },
            ready: function () {
                return this.getFutureData('recordIndex') !== undefined;
            },
            fn: function () {
                var data = this.getFutureData(),
                    instanceData = this.getInstanceData(),
                    cmp = this.future.getRelated('dataView').cmp;
 
                ST.future.Selection._doSelect(cmp, {
                    indexes: [data.recordIndex],
                    keepExisting: instanceData.keepExisting,
                    type: 'index',
                    mode: 'select'
                });
            }
        },
 
        /**
         * @method deselect
         * @chainable
         * Deselects the item's record
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         */
        deselect: {
            addEvent: function (timeout) {
                var me = this;
 
                me.play(me._buildRec('deselect',{
                    timeout: timeout
                }));
 
                return me;
            },
            ready: function () {
                return this.getFutureData('recordIndex') !== undefined;
            },
            fn: function () {
                var data = this.getFutureData(),
                    cmp = this.future.getRelated('dataView').cmp;
 
                ST.future.Selection._doSelect(cmp, {
                    indexes: [data.recordIndex],
                    type: 'index',
                    mode: 'deselect'
                });
            }
        }
    },
 
    statics: {
        _doSelect: function (cmp, config) {
            var opts = config,
                records = ST.future.SelectionModel._getRecords(cmp, opts),
                selModel;
 
            if (ST.isClassic) {
                selModel = cmp.getSelectionModel();
            } else if (ST.sdkVersion.gtEq('6.5.0')) {
                selModel = cmp.getSelectable();
            } else {
                selModel = cmp;
            }
 
            if (opts.mode === 'select') {
                selModel.select(records, opts.keepExisting);
            } else {
                selModel.deselect(records);
            }
        }
    }
    
});
 
//-----------------------------------------------------------------------------
// DataView / Item
 
/**
 * @class ST.future.Item
 * @extend ST.future.Element
 * This class provides methods to interact with a `DataView` item when it becomes
 * available. Instances of this class are returned by the following methods:
 *
 *  * {@link ST.future.DataView#item}
 *  * {@link ST.future.DataView#itemAt}
 *  * {@link ST.future.DataView#itemBy}
 *  * {@link ST.future.DataView#itemWith}
 */
ST.future.define('Item', {
    extend: ST.future.Element,
    mixins: [ST.future.Selection],
    factoryable: false,
    valueProperty: null,
 
    /**
     * @cfg {Number} at
     * The item index in the component's `store`. This property is set when calling the
     * {@link ST.future.DataView#itemAt} method.
     *
     * If specified the `itemId`, `propertyName` and `propertyValue` configs are ignored.
     */
 
    /**
     * @cfg itemId
     * The value of the `idProperty` of the item's record. This property is set when
     * calling the {@link ST.future.DataView#item} method.
     *
     * If specified the `propertyName` and `propertyValue` configs are ignored.
     */
 
    /**
     * @cfg {String} propertyName
     * The name of the property for which to search. The first record that matches the
     * desired `propertyValue` is used as returned by the store's `find()` method.
     *
     * This property is set when calling the {@link ST.future.DataView#itemWith} method.
     *
     * If specified the `propertyValue` must also be specified.
     */
 
    /**
     * @cfg {Object/RegExp} propertyValue
     * The value that must exactly match that of the `propertyName` unless this is a
     * `RegExp`.  This and the `propertyName` are passed to the store's `find()` method
     * to find the matching record.
     *
     *  This property is set when calling the {@link ST.future.DataView#itemWith} method.
     */
 
    /**
     * @method selected
     * Waits for the item's record to be selected
     *
     *      ST.dataView('@someDataView').
     *          itemAt(1).
     *          selected().
     *          and(function (item) {
     *              // item is now selected
     *          });
     *
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Item} this
     * @chainable
     */
 
    /**
     * @method deselected
     * Waits for the item's record to be deselected
     *
     *      ST.dataView('@someDataView').
     *          itemAt(150).
     *          selected().
     *          reveal().
     *          deselected().
     *          and(function (item) {
     *              // item is now deselected
     *          });
     *
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Item} this
     * @chainable
     */
 
    /**
     * @method select
     * Selects the item's corresonding record
     * @param {Boolean} [keepExisting] `true` to preserve existing selections (default: `false`)
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Item}
     */
 
    /**
     * @method deselect
     * Deselects the item's corresonding record
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Item}
     */
 
    /**
     * Returns the owning `ST.future.DataView`. This method can be called at any time
     * to "return" to the owning future. For example:
     *
     *      ST.dataView('@someView').
     *          item(42).           // get a future item (ST.future.Item)
     *              reveal().       //    operates on the ST.future.Item
     *          dataView().         // now back to the dataView
     *          click(10, 10);      // click on the dataView
     *
     * @return {ST.future.DataView}
     */
    dataView: function () {
        // replay locator to ensure context is set correctly
        this.play([this.related.dataView.locator]);
        
        return this.related.dataView;
    },
 
    playables: {
        /**
         * @method getRecord
         * @chainable
         * @since 2.2.1
         * Retrieves the Item's record data and associated data and places a shallow copy on the future instance's
         * data.record. Modifications of the record data may be reflected in the copy on the future instance.
         * @return {ST.future.Item}
         * 
         * Example usage: 
         * 
         *      ST.dataView('mydataview').itemAt(1).getRecord().and(function (item) {
         *          expect(item.data.record.species).toBe("Cat");
         * 
         *          expect(this.future.data.record.species).toBe("Cat");
         *      });
         */
        getRecord: {
            fn: function() {
                var me = this,
                    data = me.getFutureData(),
                    dataView = me.future.getRelated('dataView').cmp,
                    recordIndex = data.recordIndex,
                    record = dataView.getStore().getAt(recordIndex);
        
                data.record = ST.apply({}, record.getData(true));
            }
        },
 
        itemBy: {
            addEvent: function (config, timeout) {
                var me = this,
                    locatorChain;
 
                me.setRelated('dataView', config.dataView);
 
                locatorChain = ST.Array.slice(config.dataView.locatorChain || []);
                locatorChain.push({
                    direction: 'down',
                    futureClsName: me.$className,
                    type: 'itemBy',
                    args: {
                        item: config.item
                    }
                });
 
                me.root = locatorChain;
                me.locatorChain = locatorChain;
                me.locator = me.play(me._buildRec('itemBy', {
                    visible: null,
                    animation: false,
                    timeout: timeout,
                    root: locatorChain,
                    locatorChain: locatorChain,
                    args: {
                        item: config.item
                    }
                }));
 
                return me;
            },
            ready: function () {
                var me = this,
                    view = me.future.getRelated('dataView').cmp,
                    rec = ST.future.Item.findRecord(view, this.args.item),
                    index = view.getStore().indexOf(rec);
 
                me.future.setData('recordIndex', index);
 
                return index < 0 ? false : true;
            },
            locatorFn: function (el, args) {
                // TODO Update this when asComponent() methods are ready!!!!
                // In modern, the component may be the item itself, so we need to get the correct dataview
                var cmp = el.isComponent ? el : ST.fly(el).getComponent(),
                    rec, node, idx;
 
                rec = ST.future.Item.findRecord(cmp, args.item);
 
                if (rec) {
                    idx = cmp.getStore().indexOf(rec);
 
                    if (ST.isClassic) {
                        node = cmp.getNode(rec);
                    } else {
                        node = ST.future.Item.getItemAt(cmp, idx);
 
                        if (node) {
                            if (cmp.getUseComponents && cmp.getUseComponents()) {
                                node = node.el.dom;
                            } else if (node.isComponent) {
                                node = node.element.dom;
                            }
                        }
                    }
                }
 
                return node;
            },
            fn: function () {
                // buffered rendering means we may have a record index but no el exists
                var me = this,
                    cmp = me.future.getRelated('dataView').cmp,
                    node, rec;
 
                rec = ST.future.Item.findRecord(cmp, me.args.item);
 
                if (ST.isClassic) {
                    node = cmp.getNode(rec);
                } else {
                    node = ST.future.Item.getItemAt(cmp, cmp.getStore().indexOf(rec));
 
                    if (node) {
                        if (cmp.getUseComponents && cmp.getUseComponents()) {
                            node = node.el.dom;
                        } else if (node.isComponent) {
                            node = node.element.dom;
                        }
                    }
                }
 
                this.updateEl(node);
            }
        },
 
        /**
         * @method reveal
         * @chainable
         * Scrolls this item into view.
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Item}
         */
        reveal: {
            params: 'timeout',
            fn: function (done) {
                var me = this,
                    view = me.future.related.dataView.cmp,
                    element = me.getElement(),
                    node = element && element.dom,
                    duration = ST.options.eventDelay,
                    anim = false,
                    isTouchScroller = false,
                    doneOnScrollEnd = false,
                    allowHScroll = false,
                    scroller, scrollOpts, inView, idx;
 
                if (!node && view.ensureVisible) {
                    idx = me.getFutureData().recordIndex;
                    view.ensureVisible(idx, false).then(function (opts) {
                        me.updateEl(opts.item.el.dom);
                        done();
                    });
                    return;
                } else if (view.getScrollable) {
                    scroller = view.getScrollable();
                    isTouchScroller = scroller.isTouchScroller;
                    allowHScroll = scroller.getX ? !!scroller.getX() : allowHScroll;
                    inView = scroller.isInView ? scroller.isInView(node) : ST.future.Item.isInView(view, node);
 
                    // 5.x+
                    if (duration) {
                        anim = {
                            duration: duration,
                            callback: function () {
                                done();
                            }
                        };
                    }
                    
                    // let's check if the item is already in view; if so, we don't need to do any scrolling
                    if (inView.y && inView.x) {
                        // if it's in view, just finish
                        done();
                    }
 
                    if (!ST.isClassic && !isTouchScroller) {
                        // need to wait for scrollend
                        scroller.on('scrollend', function() {
                            done();
                        }, view, {
                            single: true
                        });
                        // clear out anim for dom scroller
                        anim = false;
                        doneOnScrollEnd = true;
                    }
 
                    scroller.scrollIntoView(node, allowHScroll, anim);                    
                } else {
                    // let's check if the item is already in view; if so, we don't need to do any scrolling
                    if (ST.future.Item.isInView(view, node).y) {
                        // if it's in view, just finish
                        done();
                        return;
                    }
 
                    scrollOpts = {
                        single: true
                    };
                    // 4.x, 5.0.x
                    if (view.onViewScroll) {
                        scrollOpts.element = 'el';
                    }
                    
                    view.on('scroll', done, view, scrollOpts);
 
                    anim = false;
                    doneOnScrollEnd = true;
 
                    Ext.fly(node).scrollIntoView(view.getOverflowEl(), allowHScroll);
                }
 
                if (!anim && !doneOnScrollEnd) {
                    done();
                }
            }
        }
    },
 
    /**
     * @method asComponent
     * Returns an `ST.future.Component` future
     * This method is *only* applicable in conjuction with Ext JS Modern Toolkit (or Sencha Touch) when 
     * using an Ext.dataview.DataView that is configured with `useComponents:true`.
     * @chainable
     * @return {ST.future.Component}
     */
    asComponent: function (timeout) {
        return this._asFutureComponent('component', timeout);
    },
 
    /**
     * @method asButton
     * Returns an `ST.future.Button` future
     * This method is *only* applicable in conjuction with Ext JS Modern Toolkit (or Sencha Touch) when 
     * using an Ext.dataview.DataView that is configured with `useComponents:true`.
     * @chainable
     * @return {ST.future.Button}
     */
    asButton: function (timeout) {
        return this._asFutureComponent('button', timeout);
    },
 
    /**
     * @method asPanel
     * Returns an `ST.future.Panel` future
     * This method is *only* applicable in conjuction with Ext JS Modern Toolkit (or Sencha Touch) when 
     * using an Ext.dataview.DataView that is configured with `useComponents:true`.
     * @chainable
     * @return {ST.future.Panel}
     */
    asPanel: function (timeout) {
        return this._asFutureComponent('panel', timeout);
    },
 
    /**
     * @method asField
     * Returns an `ST.future.Field` future
     * This method is *only* applicable in conjuction with Ext JS Modern Toolkit (or Sencha Touch) when 
     * using an Ext.dataview.DataView that is configured with `useComponents:true`.
     * @chainable
     * @return {ST.future.Field}
     */
    asField: function (timeout) {
        return this._asFutureComponent('field', timeout);
    },
 
    /**
     * @method asTextField
     * Returns an `ST.future.TextField` future
     * This method is *only* applicable in conjuction with Ext JS Modern Toolkit (or Sencha Touch) when 
     * using an Ext.dataview.DataView that is configured with `useComponents:true`.
     * @chainable
     * @return {ST.future.TextField}
     */
    asTextField: function (timeout) {
        return this._asFutureComponent('textField', timeout);
    },
 
    /**
     * @method asCheckBox
     * Returns an `ST.future.CheckBox` future
     * This method is *only* applicable in conjuction with Ext JS Modern Toolkit (or Sencha Touch) when 
     * using an Ext.dataview.DataView that is configured with `useComponents:true`.
     * @chainable
     * @return {ST.future.CheckBox}
     */
    asCheckBox: function (timeout) {
        return this._asFutureComponent('checkBox', timeout);
    },
 
    /**
     * @method asSelect
     * Returns an `ST.future.Select` future
     * This method is *only* applicable in conjuction with Ext JS Modern Toolkit (or Sencha Touch) when 
     * using an Ext.dataview.DataView that is configured with `useComponents:true`.
     * @chainable
     * @return {ST.future.Select}
     */
    asSelect: function (timeout) {
        return this._asFutureComponent('select', timeout);
    },
 
    /**
     * @method asDataView
     * Returns an `ST.future.DataView` future
     * This method is *only* applicable in conjuction with Ext JS Modern Toolkit (or Sencha Touch) when 
     * using an Ext.dataview.DataView that is configured with `useComponents:true`.
     * @chainable
     * @return {ST.future.DataView}
     */
    asDataView: function (timeout) {
        return this._asFutureComponent('dataView', timeout);
    },
 
    /**
     * @method asGrid
     * Returns an `ST.future.Grid` future
     * This method is *only* applicable in conjuction with Ext JS Modern Toolkit (or Sencha Touch) when 
     * using an Ext.dataview.DataView that is configured with `useComponents:true`.
     * @chainable
     * @return {ST.future.Grid}
     */
    asGrid: function (timeout) {
        return this._asFutureComponent('grid', timeout);
    },
 
    //-------------------------------------------------------------
    // Private
 
    /**
     * Common endpoint for creating component futures from an item.
     * @param {Object} type The type of future to create.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Component}
     */
    _asFutureComponent: function (type, timeout) {
        var me = this,
            future;
 
        // tack on an "empty" locatorChain item, since we are just borrowing this locator
        // however, we still need the "type" to determin how the locator we're borrowing will operate
        me.locator.locatorChain.push({
            locator: '$$SKIP$$',
            futureClsName: me.$className,
            type: 'component' 
        });
 
        future = ST[type](me.locator, timeout);
        future.setRelated('item', me);
 
        return future;
    },
 
    statics: {
        /**
         * @private
         */
        findRecord: function (dataView, config) {
            var store = dataView.store || dataView.getStore(),
                index = config.at,
                id = config.itemId,
                rec = null;
 
            if (index != null) {
                rec = store.getAt(index);
            } else if (id != null) {
                rec = store.getById(id);
            } else {
                rec = store.findRecord(config.propertyName, ST.decodeRegex(config.propertyValue));
            }
 
            return rec;
        },
        /**
         *  @private
         */
        isInView: function(view, node) {
            var result = {
                    x: false,
                    y: false
                },
                elRegion,
                myEl = view.el,
                myElRegion;
 
            if (node && myEl.contains(node)) {
                myElRegion = myEl.getRegion();
                elRegion = Ext.get(node).getRegion();
 
                result.x = elRegion.right > myElRegion.left && elRegion.left < myElRegion.right;
                result.y = elRegion.bottom > myElRegion.top && elRegion.top < myElRegion.bottom;
            }
 
            return result;
        },
        /**
         * @private
         */
        getItemAt: function (view, index) {
            var item = null,
                relativeIndex;
            // for 6.5 Modern, buffered views are supported, so getItemAt() won't map store index values
            // to view index values, so we need to use another approach
            if (view.mapToViewIndex) {
                try {
                    relativeIndex = view.mapToViewIndex(index);
                } catch (e) { 
                    // if the index gets out of range of rendered items, mapToViewIndex seems to throw an error
                    // so we'll resolve relativeIndex to -1 if an error occurs
                    relativeIndex = -1;
                }
 
                if (relativeIndex !== -1) { 
                    item = view.mapToItem(relativeIndex);
                }                
            } else {
                item = view.getItemAt(index);
            }
 
            return item;
        }
    }
});
 
/**
 * This class provides methods specific to Ext JS DataView (`Ext.view.View`).
 *
 * @class ST.future.DataView
 * @extend ST.future.Component
 */
ST.future.define('DataView', {
    extend: ST.future.Component,
    mixins: [ST.future.SelectionModel],
    playables: {
        /**
         * @method viewReady
         * Waits for this initial set of items to be rendered.
         *
         *      ST.dataView('@someView').
         *          viewReady().
         *          and(function (view) {
         *              // view items are now rendered
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.DataView} this
         * @chainable
         */
        viewReady: {
            is: function () {
                var cmp = this.getComponent(),
                    ready, items;
 
                if (ST.isClassic) {
                    ready = cmp.viewReady;
                } else {
                    items = cmp.getViewItems();
                    ready = items && items.length;
                }
 
                return ready;
            },
            wait: function (done) {
                var cmp = this.getComponent(),
                    intervalFn;
 
                intervalFn = setInterval(function () {
                    var items;
 
                    if (ST.isClassic && (cmp && cmp.viewReady)) {
                        done();
                    } else if (cmp.getViewItems)  {
                        items = cmp.getViewItems();                        
                        
                        if (items && items.length) {
                            clearInterval(intervalFn);
                            done();
                        }                        
                    }
                }, 100);
 
                return function () {
                    clearInterval(intervalFn);
                };
            }
        },
    },
 
    /**
     * @method selected
     * Waits for the given item records (by id) to be selected
     *
     *      ST.dataView('@someDataView').
     *          selected([1, 3]).
     *          and(function (dataView) {
     *              // 2 item records are now selected
     *          });
     *
     * @param {Number/Number[]} id The ids of the item records to select.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.DataView} this
     * @chainable
     */
 
    /**
     * @method selectedAt
     * Waits for the given item records (by index) to be selected
     *
     *      ST.dataView('@someDataView').
     *          selectedAt([1, 3]).
     *          and(function (dataView) {
     *              // 2 item records are now selected
     *          });
     *
     * @param {Number/Number[]} index The indexes of the item records to select.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.DataView} this
     * @chainable
     */
 
    /**
     * @method selectedRange
     * Waits for the given item records (by range of indexes) to be selected
     *
     *      ST.dataView('@someDataView').
     *          selectedRange(15, 45).
     *          and(function (dataView) {
     *              // range of item records are now selected
     *          });
     *
     * @param {Number} start The starting index of the item records to select.
     * @param {Number} [end] The ending index of the item records to select. 
     * If not specified, the remainder of the available records will be selected
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.DataView} this
     * @chainable
     */
 
    /**
     * @method selectedWith
     * Waits for the given item records (by simple query) to be selected
     *
     *      ST.dataView('@someDataView').
     *          selectedWith('name', 'Doug').
     *          and(function (dataView) {
     *              // matching item records are now selected
     *          });
     *
     * @param {String} propertyName The name of the property in the record against which to query.
     * @param {String} propertyValue The value against which to query.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.DataView} this
     * @chainable
     */
 
    /**
     * @method deselected
     * Waits for the given item records (by id) to be deselected
     *
     *      ST.dataView('@someDataView').
     *          ... select records ...
     *          deselected([1, 3]).
     *          and(function (dataView) {
     *              // 2 item records are now deselected
     *          });
     *
     * @param {Number/Number[]} id The ids of the item records to deselect.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.DataView} this
     * @chainable
     */
 
    /**
     * @method deselectedAt
     * Waits for the given item records (by index) to be deselected
     *
     *      ST.dataView('@someDataView').
     *          ... select records ...
     *          deselectedAt([1, 3]).
     *          and(function (dataView) {
     *              // 2 item records are now deselected
     *          });
     *
     * @param {Number/Number[]} index The indexes of the item records to deselect.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.DataView} this
     * @chainable
     */
 
    /**
     * @method deselectedRange
     * Waits for the given item records (by range of indexes) to be deselected
     *
     *      ST.dataView('@someDataView').
     *          ... select records ...
     *          deselectedRange(15, 45).
     *          and(function (dataView) {
     *              // range of item records are now deselected
     *          });
     *
     * @param {Number} start The starting index of the item records to deselect.
     * @param {Number} [end] The ending index of the item records to deselect. 
     * If not specified, the remainder of the available records will be deselected
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.DataView} this
     * @chainable
     */
 
    /**
     * @method deselectedWith
     * Waits for the given item records (by simple query) to be deselected
     *
     *      ST.dataView('@someDataView').
     *          ... select records ...
     *          deselectedWith('name', 'Doug').
     *          and(function (dataView) {
     *              // matching item records are now deselected
     *          });
     *
     * @param {String} propertyName The name of the property in the record against which to query.
     * @param {String} propertyValue The value against which to query.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.DataView} this
     * @chainable
     */
 
    /**
     * @method select
     * Selects the requested record(s) given the record's `idProperty`.
     * @param {String/String[]} id The `idProperty` of the record(s) to select.
     * @param {Boolean} [keepExisting] `true` to preserve existing selections (default: `false`)
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.DataView}
     */
 
    /**
     * @method selectAt
     * Selects the requested record(s) by index.
     * @param {String/String[]} index The index of the record(s) to select.
     * @param {Boolean} [keepExisting] `true` to preserve existing selections (default: `false`)
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.DataView}
     */
 
    /**
     * @method selectRange
     * Selects the requested record(s) by index range.
     * @param {Number} start The starting index for the selection.
     * @param {Number} [end] The ending index for the selection. If not specified, the full range from the starting index will be included
     * @param {Boolean} [keepExisting] `true` to preserve existing selections (default: `false`)
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.DataView}
     */
 
    /**
     * @method selectWith
     * Selects the requested record(s) by a simple property/value query.
     * @param {String} propertyName The name of the property by which to query.
     * @param {String} value The value by which to query.
     * @param {Boolean} [keepExisting] `true` to preserve existing selections (default: `false`)
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.DataView}
     */
 
    /**
     * @method selectAll
     * Selects all available records
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.DataView}
     */
 
    /**
     * @method deselect
     * Deselects the requested record(s) given the record's `idProperty`.
     * @param {String/String[]} id The `idProperty` of the record(s) to deselect.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.DataView}
     */
 
    /**
     * @method deselectAt
     * Deselects the requested record(s) by index.
     * @param {String/String[]} index The index of the record(s) to deselect.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.DataView}
     */
 
    /**
     * @method deselectRange
     * Deselects the requested record(s) by index range.
     * @param {Number} start The starting index for the deselection.
     * @param {Number} [end] The ending index for the deselection. If not specified, the full range from the starting index will be included.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.DataView}
     */
 
    /**
     * @method deselectWith
     * Deselects the requested record(s) by a simple property/value query.
     * @param {String} propertyName The name of the property by which to query.
     * @param {String} value The value by which to query.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.DataView}
     */
 
    /**
     * @method deselect
     * Deselects all selected records
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.DataView}
     */
 
    /**
     * @method selectBy
     * Selects records given a config object that specified the match criteria.
     * @param {Object} config Configuration options for the selection.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.DataView}
     */
 
    /**
     * @method deselectBy
     * Deselects records given a config object that specified the match criteria.
     * @param {Object} config Configuration options for the deselection.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.DataView}
     */
 
 
    _itemCalls: 0,
 
    /**
     * Returns the `{@link ST.future.Item future item}` given the record's `idProperty`.
     * See {@link ST.future.Item#itemId}.
     * @param id The `idProperty` of the item's record.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Item}
     */
    item: function (id, timeout) {
        return this.itemBy({
            itemId: id
        }, timeout);
    },
 
    /**
     * Returns the `{@link ST.future.Item future item}` given the record index.
     * See {@link ST.future.Item#at}.
     * @param {Number} index The index of the item in the` component's `store`.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Item}
     */
    itemAt: function (index, timeout) {
        return this.itemBy({
            at: index
        }, timeout);
    },
 
    /**
     * Returns the `{@link ST.future.Item future item}` given a config object that
     * specified the match criteria.
     * @param {Object} config Configuration options for the `ST.future.Item`.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Item}
     */
    itemBy: function (config, timeout) {
        var me = this,
            item = new ST.future.Item();
 
        if (!me._itemCalls++) {
            me.viewReady();
        }
 
        return item.itemBy({
            dataView: me,
            item: config
        }, timeout);
    },
 
    /**
     * Returns the `{@link ST.future.Item future item}` given the name of the property/field
     * and the match value.
     * See {@link ST.future.Item#propertyName} and {@link ST.future.Item#propertyValue}.
     * @param {String} property
     * @param {Object/RegExp} value
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Item}
     */
    itemWith: function (property, value, timeout) {
        return this.itemBy({
            propertyName: property,
            propertyValue: ST.encodeRegex(value)
        }, timeout);
    }
});
 
/**
 * Returns a {@link ST.future.DataView future DataView} used to queue operations for
 * when that `Ext.view.View` becomes available.
 * @param {String} locator See {@link ST.Locator} for supported syntax.
 * @param {Number} [timeout] The maximum time (in milliseconds) to wait for the dataview.
 * @return {ST.future.DataView}
 * @method dataView
 * @member ST
 */
 
//-----------------------------------------------------------------------------
// Grid / Row / Cell
 
/**
 * @class ST.future.Cell
 * @extend ST.future.Element
 * This class provides methods to interact with a `Grid` cell when it becomes
 * available. Instances of this class are returned by the following methods:
 *
 *  * {@link ST.future.Row#cell}
 *  * {@link ST.future.Row#cellAt}
 *  * {@link ST.future.Row#cellBy}
 *  * {@link ST.future.Row#cellWith}
 */
ST.future.define('Cell', {
    extend: ST.future.Element,
    factoryable: false,
    valueProperty: null,
 
    /**
     * @cfg {Number} at
     * The column index in the grid's `columns`. This property is set when calling the
     * {@link ST.future.Row#cellAt} method.
     *
     * If specified the `cellId`, `propertyName` and `propertyValue` configs are ignored.
     */
 
    /**
     * @cfg cellId
     * The value of the `idProperty` of the row's record. This property is set when
     * calling the {@link ST.future.Grid#row} method.
     *
     * If specified the `propertyName` and `propertyValue` configs are ignored.
     */
 
    /**
     * @cfg {String} propertyName
     * The name of the property for which to search. The first record that matches the
     * desired `propertyValue` is used. This property is set when calling the
     * {@link ST.future.Grid#rowWith} method.
     *
     * If specified the `propertyValue` must also be specified.
     */
 
    /**
     * @cfg {Object/RegExp} propertyValue
     * The value that must exactly match that of the `propertyName` unless this is a
     * `RegExp`. In this case, the `test()` method is used to compare the field values.
     *  This property is set when calling the {@link ST.future.Grid#rowWith} method.
     */
 
    /**
     * Returns the owning `ST.future.Grid`. This method can be called at any time
     * to "return" to the owning future. For example:
     *
     *      ST.grid('@someGrid').
     *          row(42).            // get a future row (ST.future.Row)
     *              cellAt(3).      // cell index 3 (0-based)
     *                  reveal().   //    operates on the ST.future.Cell
     *          grid().             // now back to the grid
     *          click(10, 10);      // click on the grid
     *
     * @return {ST.future.Grid}
     */
    grid: function () {
        var grid = this.getRelated('grid');
        // replay locator to ensure context is updated appropriately
        this.play([grid.locator]);
        
        return grid;
    },
 
    playables: {
        cellBy: {
            addEvent: function (config, timeout) {
                var me = this,
                    locatorChain;
 
                me.setRelated('grid', config.grid);
                me.setRelated('row', config.row);
                
                locatorChain = ST.Array.slice(config.row.locatorChain || []);
                locatorChain.push({
                    direction: 'down',
                    futureClsName: me.$className,
                    type: 'cellBy',
                    args: {
                        cell: config.cell,
                        row: config.row.locator.args.row
                    }
                });
 
                me.root = locatorChain;
                me.locatorChain = locatorChain;
 
 
                me.locator = me.play(me._buildRec('cellBy', {
                    visible: null,
                    animation: false,
                    timeout: timeout,
                    root: locatorChain,
                    locatorChain: locatorChain,
                    args: {
                        cell: config.cell
                    }
                }));
 
                return me;
            },
            ready: function () {
                var cmp = this.future.getRelated('grid').cmp,
                    index = ST.future.Cell.findColIndex(cmp, this.args.cell);
 
                if (index < 0) {
                    return false;
                }
 
                this.future.setData('columnIndex', index);
 
                return true;
            },
            locatorFn: function (el, args) {
                var cmp = el.isComponent ? el : ST.fly(el).getComponent(),
                    // in modern, the "row" will be a component, so retrieve its grid
                    cmp = cmp.isXType('gridrow') ? cmp.up('grid') : cmp,
                    columnIndex = ST.future.Cell.findColIndex(cmp, args.cell),
                    rowIndex = ST.future.Row.findRowIndex(cmp, args.row),
                    cellInfo = ST.future.Grid.getCellInfo(cmp, columnIndex, rowIndex);
 
                return cellInfo.cell;
            },
            fn: function () {
                var data = this.getFutureData(),
                    cmp = this.future.getRelated('grid').cmp,
                    rowData = this.future.getRelated('row').data,
                    el;
 
                var el = ST.future.Grid.getCellInfo(cmp, data.columnIndex, rowData.recordIndex);
 
                el = el && el.cell;
                el = el && el.dom;
 
                this.updateEl(el);
            }
        },
        /**
         * @method reveal
         * @chainable
         * Scrolls this cell into view.
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Cell}
         */
        reveal: {
            params: 'timeout',
            fn: function (done) {
                var me = this,
                    data = me.getFutureData(),
                    grid = me.future.getRelated('grid').cmp,
                    recordIndex = me.future.getRelated('row').getData('recordIndex'),
                    colIndex = data.columnIndex;
 
                ST.future.Grid.scrollTo(grid, recordIndex, colIndex, function (cellEl) {
                    ST.defer(function () {
                        me.updateEl(cellEl);
                        done();
                    },1);
                });
            }
        }
    },
 
    /**
     * Returns the owning `ST.future.Row`. This method can be called at any time
     * to "return" to the owning row future. For example:
     *
     *      ST.grid('@someGrid').
     *          row(42).                // get a future row (ST.future.Row)
     *              cellAt(3).          // cell index 3 (0-based)
     *                  reveal().       //    operates on the ST.future.Cell
     *          row().                  // now back to the row
     *              cellAt(7).
     *                  click(10, 10);  // click on the cell index 7
     *
     * @return {ST.future.Row}
     */
    row: function () {
        var row = this.getRelated('row');
        // replay locator to ensure context is updated correctly
        this.play([row.locator]);
 
        return row;
    },
 
    //-------------------------------------------------------------
    // Private
 
    statics: {
        toolkitConfig: {
            classic: {
                findColIndex: function (grid, config) {
                    var index = config.at,
                        id = config.cellId,
                        propertyValue = ST.decodeRegex(config.propertyValue),
                        isRegex = ST.typeOf(propertyValue) === 'regexp',
                        ret = null,
                        col, columns, i, v;
 
                    if (grid.getVisibleColumnManager) {
                        // This appeared in later 4.x
                        columns = grid.getVisibleColumnManager().getColumns();
                    } else {
                        // Top level view has getGridColumns
                        columns = grid.getView().getGridColumns();
                    }
 
                    if (index != null) {  // cellAt(n)
                        if (index < 0) {
                            // map index -1 to last cell
                            index += columns.length;
                        }
                        if (0 <= index && index < columns.length) {
                            ret = index;
                        }
                    } else if (id != null) {  // cell(id)
                        for (i = 0; i < columns.length; ++i) {
                            col = columns[i];
 
                            if (id === col.id || id === col.itemId || id === col.reference) {
                                ret = i;
                                break;
                            }
                        }
                    } else {  // cellWith(prop, val)
                        for (i = 0; i < columns.length; ++i) {
                            v = columns[i][config.propertyName];
 
                            if (isRegex ? propertyValue.test(v) : (v === propertyValue)) {
                                ret = i;
                                break;
                            }
                        }
                    }
 
                    return ret;
                }
            },
 
            modern: {
                findColIndex: function (grid, config) {
                    var index = config.at,
                        id = config.cellId,
                        propertyValue = ST.decodeRegex(config.propertyValue),
                        isRegex = ST.typeOf(propertyValue) === 'regexp',
                        columns = grid.getHeaderContainer().getColumns(),
                        ret = null,
                        col, i, v, getter;
 
                    if (index != null) {  // cellAt(n)
                        if (index < 0) {
                            // map index -1 to last cell
                            index += columns.length;
                        }
                        if (0 <= index && index < columns.length) {
                            ret = index;
                        }
                    } else if (id != null) {  // cell(id)
                        for (i = 0; i < columns.length; ++i) {
                            col = columns[i];
 
                            if (id === col.id || id === col.getItemId() || id === col.getReference()) {
                                ret = i;
                                break;
                            }
                        }
                    } else {  // cellWith(prop, val)
                        for (i = 0; i < columns.length; ++i) {
                            col = columns[i];
                            getter = col['get' + Ext.String.capitalize(config.propertyName)];
                            v = getter ? getter.call(col) : col[config.propertyName];
 
                            if (isRegex ? propertyValue.test(v) : (v === propertyValue)) {
                                ret = i;
                                break;
                            }
                        }
                    }
 
                    return ret;
                }
            }
        }
    }
}); // Cell
 
/**
 * @class ST.future.Row
 * @extend ST.future.Element
 * This class provides methods to interact with a `Grid` row when it becomes
 * available. Instances of this class are returned by the following methods:
 *
 *  * {@link ST.future.Grid#row}
 *  * {@link ST.future.Grid#rowAt}
 *  * {@link ST.future.Grid#rowBy}
 *  * {@link ST.future.Grid#rowWith}
 */
ST.future.define('Row', {
    extend: ST.future.Element,
    mixins: [ST.future.Selection],
    factoryable: false,
    valueProperty: null,
 
    /**
     * @cfg {Number} at
     * The row index in the grid's `store`.
     *
     * This property is set when calling the {@link ST.future.Grid#rowAt} method.
     *
     * If specified the `rowId`, `propertyName` and `propertyValue` configs are ignored.
     */
 
    /**
     * @cfg rowId
     * The value of the `idProperty` of the row's record.
     *
     * This property is set when calling the {@link ST.future.Grid#row} method.
     *
     * If specified the `propertyName` and `propertyValue` configs are ignored.
     */
 
    /**
     * @cfg {String} propertyName
     * The name of the property for which to search. The first record that matches the
     * desired `propertyValue` is used as returned by the store's `find()` method.
     *
     * This property is set when calling the {@link ST.future.Grid#rowWith} method.
     *
     * If specified the `propertyValue` must also be specified.
     */
 
    /**
     * @cfg {Object/RegExp} propertyValue
     * The value that must exactly match that of the `propertyName` unless this is a
     * `RegExp`. This and the `propertyName` are passed to the store's `find()` method
     * to find the matching record.
     *
     *  This property is set when calling the {@link ST.future.Grid#rowWith} method.
     */
 
    /**
     * @method selected
     * Waits for the row's record to be selected
     *
     *      ST.grid('@someGrid').
     *          rowAt(1).
     *          select().
     *          selected().
     *          and(function (row) {
     *              // row is now selected
     *          });
     *
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Row} this
     * @chainable
     */
 
    /**
     * @method deselected
     * Waits for the row's record to be deselected
     *
     *      ST.grid('@someGrid').
     *          rowAt(150).
     *          select().
     *          selected().
     *          reveal().
     *          deselect().
     *          deselected().
     *          and(function (row) {
     *              // row is now deselected
     *          });
     *
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Row} this
     * @chainable
     */
 
    /**
     * Returns the `{@link ST.future.Cell future cell}` given the record's `idProperty`.
     * See {@link ST.future.Cell#rowId}.
     * @param id The `idProperty` of the item's record.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Cell}
     */
    cell: function (id, timeout) {
        return this.cellBy({
            cellId: id
        }, timeout);
    },
 
    /**
     * Returns the `{@link ST.future.Cell future cell}` given the record index.
     * See {@link ST.future.Cell#at}.
     * @param {Number} index The index of the item in the grid's `store`.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Cell}
     */
    cellAt: function (index, timeout) {
        return this.cellBy({
            at: index
        }, timeout);
    },
 
    /**
     * Returns the `{@link ST.future.Cell future cell}` given a config object that
     * specified the match criteria.
     * @param {Object} config Configuration options for the `ST.future.Cell`.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Cell}
     */
    cellBy: function (config, timeout) {
        var me = this,
            cell = new ST.future.Cell();
 
        return cell.cellBy({
            grid: me.getRelated('grid'),
            row: me,
            cell: config
        }, timeout);
    },
 
    /**
     * Returns the `{@link ST.future.Cell future cell}` given the name of the property/field
     * and the match value.
     * See {@link ST.future.Cell#propertyName} and {@link ST.future.Cell#propertyValue}.
     * @param {String} property
     * @param {Object/RegExp} value
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Cell}
     */
    cellWith: function (property, value, timeout) {
        return this.cellBy({
            propertyName: property,
            propertyValue: ST.encodeRegex(value)
        }, timeout);
    },
 
    /**
     * Returns the owning `ST.future.Grid`. This method can be called at any time
     * to "return" to the owning future. For example:
     *
     *      ST.grid('@someGrid').
     *          row(42).            // get a future row (ST.future.Row)
     *              reveal().       //    operates on the ST.future.Row
     *          grid().             // now back to the grid
     *          click(10, 10);      // click on the grid
     *
     * @return {ST.future.Grid}
     */
    grid: function () {
        var grid = this.getRelated('grid');
        // replay locator to ensure context is updated appropriately
        this.play([grid.locator]);
        
        return grid;
    },
 
    /**
     * @method select
     * Selects the row's corresonding record
     * @param {Boolean} [keepExisting] `true` to preserve existing selections (default: `false`)
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Row}
     */
 
    /**
     * @method deselect
     * Deselects the row's corresonding record
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Row}
     */
 
    playables: {
        rowBy: {
            addEvent: function (config, timeout) {
                var me = this,
                    locatorChain;
 
                me.setRelated('grid', config.grid);
                me.setRelated('dataView', config.grid);
 
                locatorChain = ST.Array.slice(config.grid.locatorChain || []);
                locatorChain.push({
                    direction: 'down',
                    futureClsName: me.$className,
                    type: 'rowBy',
                    args: {
                        row: config.row
                    }
                });
 
                me.root = locatorChain;
                me.locatorChain = locatorChain;
 
                me.locator = me.play(me._buildRec('rowBy', {
                    visible: null,
                    animation: false,
                    timeout: timeout,
                    root: locatorChain,
                    locatorChain: locatorChain,
                    args: {
                        row: config.row
                    }
                }));
 
                return me;
            },
            ready: function () {
                var me = this,
                    grid = me.future.getRelated('grid').cmp, 
                    index = ST.future.Row.findRowIndex(grid, this.args.row);
 
                me.future.setData('recordIndex', index);
 
                return index < 0 ? false : true;
            },
            locatorFn: function (el, args) {
                var view = el.isComponent ? el : ST.fly(el).getComponent(), 
                    index = ST.future.Row.findRowIndex(view, args.row),
                    node;
 
                if (ST.isClassic) {
                    view = (view.normalGrid || view).view;
                    node = view.getNode(index); 
                } else {
                    node = ST.future.Row.getItemAt(view, index);
 
                    if (node) {
                        node = node.el;
                    }
                }
 
                return node;
            },
            fn: function () {
                // buffered rendering means we may have a record index but no el exists
                var data = this.getFutureData(),
                    view = this.future.getRelated('grid').cmp,
                    node;
 
                if (ST.isClassic) {
                    view = (view.normalGrid || view).view;
                    node = view.getNode(data.recordIndex); 
                } else {
                    node = ST.future.Row.getItemAt(view, data.recordIndex);
 
                    if (node) {
                        node = node.el;
                    }
                }
 
                this.updateEl(node);
            }
        },
        /**
         * @method getRecord
         * @chainable
         * Retrieves the Row's record data and associated data and places a shallow copy on the future instance's
         * data.record. Modifications of the record data may be reflected in the copy on the future instance.
         * @return {ST.future.Row}
         * 
         * Given a grid with row model records that inclue a "species" property and the grid store includes
         * records with the values "Cat" and "Dog". 
         * 
         * +---------+
         * | Species |
         * |---------|
         * |   Cat   |
         * |   Dog   |
         * +---------+
         * 
         *      ST.grid('mygrid').rowAt(1).getRecord().and(function (row) {
         *          expect(row.data.record.species).toBe("Cat");
         *          expect(row.data.record.species).toBe("Dog");
         * 
         *          expect(this.future.data.record.species).toBe("Cat");
         *          expect(this.future.data.record.species).toBe("Dog");
         *      });
         */
        getRecord: {
            fn: function () {
                var me = this,
                    data = me.getFutureData(),
                    grid = me.future.getRelated('grid').cmp,
                    recordIndex = data.recordIndex,
                    record = grid.getStore().getAt(recordIndex);
 
                data.record = ST.apply({}, record.getData(true));
            }
        },
        /**
         * @method reveal
         * @chainable
         * Scrolls this row into view.
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Row}
         */
        reveal: {
            params: 'timeout',
            fn: function (done) {
                var me = this,
                    data = me.getFutureData(),
                    grid = me.future.getRelated('grid').cmp,
                    recordIndex = data.recordIndex;
 
                ST.future.Grid.scrollTo(grid, recordIndex, null, function (rowEl) {
                    ST.defer(function () {
                        me.updateEl(rowEl);
                        done();
                    },1);
                });
            }
        }
    },
 
    //-------------------------------------------------------------
    // Private
 
    statics: {
        /**
         * @private
         */
        findRowIndex: function (grid, config) {
            var store = grid.store || grid.getStore(),
                count = store.getCount(),
                index = config.at,
                id = config.itemId,
                ret = -1;
 
            if (index != null) {
                if (index < 0) {
                    // map -1 to last record
                    index += count;
                }
                if (0 <= index && index < count) {
                    ret = index;
                }
            } else if (id != null) {
                ret = store.indexOfId(id);
            } else {
                ret = store.find(config.propertyName, config.propertyValue);
            }
 
            // TODO IS THIS USED????
            /*if (ret >= 0) {
                me.record = store.getAt(ret);
            }*/
 
            return ret;
        },
        /**
         * @private
         */
        getItemAt: function (view, index) {
            var item = null,
                relativeIndex;
            // for 6.5 Modern, buffered views are supported, so getItemAt() won't map store index values
            // to view index values, so we need to use another approach
            if (view.mapToViewIndex) {
                try {
                    relativeIndex = view.mapToViewIndex(index);
                } catch (e) { 
                    // if the index gets out of range of rendered items, mapToViewIndex seems to throw an error
                    // so we'll resolve relativeIndex to -1 if an error occurs
                    relativeIndex = -1;
                }
                
 
                if (relativeIndex !== -1) { 
                    item = view.mapToItem(relativeIndex);
                }                
            } else {
                item = view.getItemAt(index);
            }
 
            return item;
        }
    }
}); // Row
 
/**
 * This class provides methods specific to Ext JS Grid (`Ext.grid.Panel`, `Ext.grid.Grid`).
 * 
 * ## Examples
 *
 *      // Locating a grid row by index
 *      ST.grid('#mygrid').
 *          rowAt(2).
 *          cellAt(1).
 *          textLike(/Sencha/i);
 * 
 *      // Locating a grid row by field name and value
 *      ST.grid('#mygrid').
 *          rowWith('company', 'Sencha').
 *          cellAt(3).
 *          textLike(/United States/i);
 * 
 *      // Locating a grid row and getting its record data
 *      ST.grid('#mygrid').
 *          rowAt(2).
 *          getRecord().
 *          and(function (row) {
 *              expect(row.data.record.company).toBe('Sencha');
 * 
 *              expect(this.future.data.record.company).toBe('Sencha');
 *          });
 *
 * ## Futures
 * 
 * The general mechanics of futures is covered in {@link ST.future.Element}.
 * 
 * ## Locators
 * 
 * See {@link ST.Locator} for supported locator syntax.
 * 
 * @class ST.future.Grid
 * @extend ST.future.Panel
 */
ST.future.define('Grid', {
    extend: ST.future.Panel,
    mixins: [ST.future.SelectionModel],
    playables: {
        /**
         * @method viewReady
         * Waits for this initial set of rows to be rendered.
         *
         *      ST.grid('@someGrid').
         *          viewReady().
         *          and(function (grid) {
         *              // grid rows are now rendered
         *          });
         *
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Grid} this
         * @chainable
         */
        viewReady: {
            is: function () {
                var cmp = this.getComponent(),
                    items, ready;
 
                cmp = cmp && (cmp.normalGrid || cmp);
 
                cmp = ST.isClassic ? (cmp && cmp.view) : cmp;
 
                if (ST.isClassic) {
                    ready = (cmp && cmp.viewReady);
                } else {
                    items = cmp.getViewItems();
                    ready = items && items.length;
                }
 
                return ready;
            },
            wait: function (done) {
                var cmp = this.getComponent(),
                    intervalFn;
 
                intervalFn = setInterval(function () {
                    var items;
 
                    if (ST.isClassic && cmp && cmp.view && cmp.view.viewReady) {
                        done();
                    } else if (cmp.getViewItems)  {
                        items = cmp.getViewItems();
 
                        if (items && items.length) {
                            clearInterval(intervalFn);
                            done();
                        }                        
                    }
                }, 100);
 
                return function () {
                    clearInterval(intervalFn);
                };
            }
        }
 
        /**
         * @method selected
         * Waits for the given row records (by id) to be selected
         *
         *      ST.grid('@someGrid').
         *          selected([1, 3]).
         *          and(function (grid) {
         *              // 2 row records are now selected
         *          });
         *
         * @param {Number/Number[]} id The ids of the row records to select.
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Grid} this
         * @chainable
         */
 
        /**
         * @method selectedAt
         * Waits for the given row records (by index) to be selected
         *
         *      ST.grid('@someGrid').
         *          selectedAt([1, 3]).
         *          and(function (grid) {
         *              // 2 row records are now selected
         *          });
         *
         * @param {Number/Number[]} index The indexes of the row records to select.
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Grid} this
         * @chainable
         */
 
        /**
         * @method selectedRange
         * Waits for the given row records (by range of indexes) to be selected
         *
         *      ST.grid('@someGrid').
         *          selectedRange(15, 45).
         *          and(function (grid) {
         *              // range of row records are now selected
         *          });
         *
         * @param {Number} start The starting index of the row records to select.
         * @param {Number} [end] The ending index of the row records to select. 
         * If not specified, the remainder of the available records will be selected
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Grid} this
         * @chainable
         */
 
        /**
         * @method selectedWith
         * Waits for the given row records (by simple query) to be selected
         *
         *      ST.grid('@someGrid').
         *          selectedWith('name', 'Doug').
         *          and(function (grid) {
         *              // matching row records are now selected
         *          });
         *
         * @param {String} propertyName The name of the property in the record against which to query.
         * @param {String} propertyValue The value against which to query.
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Grid} this
         * @chainable
         */
 
        /**
         * @method deselected
         * Waits for the given row records (by id) to be deselected
         *
         *      ST.grid('@someGrid').
         *          ... select records ...
         *          deselected([1, 3]).
         *          and(function (grid) {
         *              // 2 row records are now deselected
         *          });
         *
         * @param {Number/Number[]} id The ids of the row records to deselect.
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Grid} this
         * @chainable
         */
 
        /**
         * @method deselectedAt
         * Waits for the given row records (by index) to be deselected
         *
         *      ST.grid('@someGrid').
         *          ... select records ...
         *          deselectedAt([1, 3]).
         *          and(function (grid) {
         *              // 2 row records are now deselected
         *          });
         *
         * @param {Number/Number[]} index The indexes of the row records to deselect.
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Grid} this
         * @chainable
         */
 
        /**
         * @method deselectedRange
         * Waits for the given row records (by range of indexes) to be deselected
         *
         *      ST.grid('@someGrid').
         *          ... select records ...
         *          deselectedRange(15, 45).
         *          and(function (grid) {
         *              // range of row records are now deselected
         *          });
         *
         * @param {Number} start The starting index of the row records to deselect.
         * @param {Number} [end] The ending index of the row records to deselect. 
         * If not specified, the remainder of the available records will be deselected
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Grid} this
         * @chainable
         */
 
        /**
         * @method deselectedWith
         * Waits for the given row records (by simple query) to be deselected
         *
         *      ST.grid('@someGrid').
         *          ... select records ...
         *          deselectedWith('name', 'Doug').
         *          and(function (grid) {
         *              // matching row records are now deselected
         *          });
         *
         * @param {String} propertyName The name of the property in the record against which to query.
         * @param {String} propertyValue The value against which to query.
         * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
         * @return {ST.future.Grid} this
         * @chainable
         */
    },
 
    _itemCalls: 0,
 
    /**
     * @method select
     * Selects the requested record(s) given the record's `idProperty`.
     * @param {String/String[]} id The `idProperty` of the record(s) to select.
     * @param {Boolean} [keepExisting] `true` to preserve existing selections (default: `false`)
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Grid}
     */
 
    /**
     * @method selectAt
     * Selects the requested record(s) by index.
     * @param {String/String[]} index The index of the record(s) to select.
     * @param {Boolean} [keepExisting] `true` to preserve existing selections (default: `false`)
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Grid}
     */
 
    /**
     * @method selectRange
     * Selects the requested record(s) by index range.
     * @param {Number} start The starting index for the selection.
     * @param {Number} [end] The ending index for the selection. If not specified, the full range from the starting index will be included
     * @param {Boolean} [keepExisting] `true` to preserve existing selections (default: `false`)
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Grid}
     */
 
    /**
     * @method selectWith
     * Selects the requested record(s) by a simple property/value query.
     * @param {String} propertyName The name of the property by which to query.
     * @param {String} value The value by which to query.
     * @param {Boolean} [keepExisting] `true` to preserve existing selections (default: `false`)
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Grid}
     */
 
    /**
     * @method selectAll
     * Selects all available records
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Grid}
     */
 
    /**
     * @method deselect
     * Deselects the requested record(s) given the record's `idProperty`.
     * @param {String/String[]} id The `idProperty` of the record(s) to deselect.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Grid}
     */
 
    /**
     * @method deselectAt
     * Deselects the requested record(s) by index.
     * @param {String/String[]} index The index of the record(s) to deselect.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Grid}
     */
 
    /**
     * @method deselectRange
     * Deselects the requested record(s) by index range.
     * @param {Number} start The starting index for the deselection.
     * @param {Number} [end] The ending index for the deselection. If not specified, the full range from the starting index will be included.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Grid}
     */
 
    /**
     * @method deselectWith
     * Deselects the requested record(s) by a simple property/value query.
     * @param {String} propertyName The name of the property by which to query.
     * @param {String} value The value by which to query.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Grid}
     */
 
    /**
     * @method deselectAll
     * Deselects all selected records
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Grid}
     */
 
    /**
     * @method selectBy
     * Selects records given a config object that specified the match criteria.
     * @param {Object} config Configuration options for the selection.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Grid}
     */
 
    /**
     * @method deselectBy
     * Deselects records given a config object that specified the match criteria.
     * @param {Object} config Configuration options for the deselection.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Grid}
     */
 
    /**
     * Returns the `{@link ST.future.Row future row}` given the record's `idProperty`.
     * See {@link ST.future.Row#rowId}.
     * @param id The `idProperty` of the item's record.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Row}
     */
    row: function (id, timeout) {
        return this.rowBy({
            itemId: id
        }, timeout);
    },
 
    /**
     * Returns the `{@link ST.future.Row future row}` given the record index.
     * See {@link ST.future.Row#at}.
     * @param {Number} index The index of the item in the grid's `store`.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Row}
     */
    rowAt: function (index, timeout) {
        return this.rowBy({
            at: index
        }, timeout);
    },
 
    /**
     * Returns the `{@link ST.future.Row future row}` given a config object that
     * specified the match criteria.
     * @param {Object} config Configuration options for the `ST.future.Row`.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Row}
     */
    rowBy: function (config, timeout) {
        var me = this,
            row = new ST.future.Row();
 
        if (!me._itemCalls++) {
            me.viewReady();
        }
 
        return row.rowBy({
            grid: me,
            dataView: me,
            row: config
        }, timeout);
    },
 
    /**
     * Returns the `{@link ST.future.Row future row}` given the name of the property/field
     * and the match value.
     * See {@link ST.future.Row#propertyName} and {@link ST.future.Row#propertyValue}.
     * @param {String} property
     * @param {Object/RegExp} value
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Row}
     */
    rowWith: function (property, value, timeout) {
        return this.rowBy({
            propertyName: property,
            propertyValue: value
        }, timeout);
    },
 
    //------------------------------------
    statics: {
        toolkitConfig: {
            classic: {
                getCellInfo: function (grid, colIndex, row) {
                    var cell, column, columns, context, info, view;
 
                    if (grid.getVisibleColumnManager) {
                        // This appeared in later 4.x
                        columns = grid.getVisibleColumnManager().getColumns();
                    } else {
                        // Top level view has getGridColumns
                        columns = grid.getView().getGridColumns();
                    }
 
                    column = columns[colIndex];
                    view = column.getView ? column.getView() : column.up('tablepanel').getView();
 
                    info = {
                        column: column,
                        view: view
                    };
 
                    if (row != null) {
                        if (Ext.grid.CellContext) {
                            // CellContext appeared in later 4.x
                            info.context = context = new Ext.grid.CellContext(view);
                            context.setPosition(row, column);
 
                            // 6+ has a getCell method
                            if (context.getCell) {
                                cell = context.getCell();
                            } else {
                                cell = view.getCellByPosition(context);
                            }
 
                            cell = view.getCellByPosition(context);
                        } else {
                            // Use the view
                            cell = view.getCellByPosition({
                                row: row,
                                column: colIndex
                            });
                        }
 
                        info.cell = cell;
                    }
 
                    return info;
                },
                scrollTo: function (grid, rowIndex, colIndex, callback) {
                    var view, column, viewItem, cellContext, cell, bufferedRenderer;
 
                    // A non-null col index was typed in. We're going to a cell
                    if (colIndex != null) {
                        if (grid.getVisibleColumnManager) {
                            // This appeared in later 4.x
                            column = grid.getVisibleColumnManager().getColumns()[colIndex];
                        } else {
                            // Top level view has getGridColumns
                            column = grid.getView().getGridColumns()[colIndex];
                        }
 
                        // Might be a lockable so get the view which owns the column.
                        view = column.getView ? column.getView() : column.up('tablepanel').getView();
 
                        // 5.1+ with ensureVisible API
                        // Use the grid that owns the column. Calling from the lockable does *two*
                        // calls, one for each side which may not be desired.
                        if (grid.ensureVisible) {
                            view.grid.ensureVisible(rowIndex, {
                                // 6.x scrolls this into view. 5.x does not use this option
                                column: column,
 
                                callback: function (success, record, viewItem) {
                                    // CellContext.setPosition(rec/recIdx, col/colIdx); rec,col is
                                    // best rather than indices
                                    cellContext = new Ext.grid.CellContext(column.getView());
                                    cellContext.setPosition(record, column);
 
                                    // 6+ has a getCell method
                                    if (cellContext.getCell) {
                                        cell = cellContext.getCell();
                                    } else {
                                        // 5.x need the view's intervention to scroll the cell
                                        // into view
                                        cell = view.getCellByPosition(cellContext);
                                    }
 
                                    view.getScrollable().scrollIntoView(cell);
                                    callback(cell);
                                }
                            });
                        } else { // 4.x/5.0
                            // 4.x algorithm scrolls requested record to the *top* of the grid
                            if (view.bufferedRenderer) {
                                view.bufferedRenderer.scrollTo(rowIndex, false, function (recIndex, record) {
                                    //viewItem = view.getNode(record);
 
                                    // CellContext appeared in later 4.x
                                    if (Ext.grid.CellContext) {
                                        cellContext = new Ext.grid.CellContext(view);
                                        cellContext.setPosition(record, column);
 
                                        cell = view.getCellByPosition(cellContext);
                                    } else {
                                        // Use the view
                                        cell = view.getCellByPosition({
                                            row: record,
                                            column: colIndex
                                        });
                                    }
 
                                    // Scroller appeared in later 4.x
                                    if (view.getScrollable) {
                                        view.getScrollable().scrollIntoView(cell);
                                    } else {
                                        Ext.fly(cell).scrollIntoView(view.getOverflowEl());
                                        //view.getOverflowEl().scrollChildIntoView(cell);
                                    }
 
                                    callback(cell);
                                });
                            } else {
                                // Element based scrolling scrolls target el just into view
                                viewItem = view.getNode(rowIndex);
                                Ext.fly(viewItem).scrollIntoView(view.getOverflowEl(), false);
                                //view.getOverflowEl().scrollChildIntoView(viewItem, false);
 
                                // CellContext appeared in later 4.x
                                if (Ext.grid.CellContext) {
                                    cellContext = new Ext.grid.CellContext(view).setPosition(rowIndex, column);
                                    cell = view.getCellByPosition(cellContext);
                                } else {
                                    // Use the view
                                    cell = view.getCellByPosition({row: rowIndex, column: colIndex});
                                }
 
                                // Scroller appeared in later 4.x
                                if (view.getScrollable) {
                                    view.getScrollable().scrollIntoView(cell);
                                } else {
                                    Ext.fly(cell).scrollIntoView(view.getOverflowEl());
                                    //view.getOverflowEl().scrollChildIntoView(cell);
                                }
 
                                callback(cell);
                            }
                        }
                    } else { // Row only
                        // 5.1+ with ensureVisible API
                        // Use the grid that owns the column. Calling from the lockable does *two*
                        // calls, one for each side which may not be desired.
                        if (grid.ensureVisible) {
                            grid.ensureVisible(rowIndex, {
                                callback: function (success, record, viewItem) {
                                    callback(viewItem);
                                }
                            });
                        } else {
                            view = grid.normalGrid || grid;
                            view = grid.view;
 
                            bufferedRenderer = view.bufferedRenderer || (grid.normalGrid && grid.normalGrid.bufferedRenderer);
                            // 4.x algorithm scrolls requested record to the *top* of the grid
                            if (bufferedRenderer) {
                                bufferedRenderer.scrollTo(rowIndex, false, function (recIndex, record) {
                                    var viewItem = view.getNode(record);
                                    callback(viewItem);
                                });
                            } else {
                                // Element based scrolling scrolls target el just into view
                                var viewItem = view.getNode(rowIndex);
 
                                Ext.fly(viewItem).scrollIntoView(view.getOverflowEl(), false);
                                //view.getOverflowEl().scrollChildIntoView(viewItem, false);
 
                                callback(viewItem);
                            }
                        }
                    }
                }
            },
            modern: {
                getCellInfo: function (grid, colIndex, row) {
                    var column, columns, info;
 
                    columns = grid.getColumns();
 
                    column = columns[colIndex];
 
                    info = {
                        column: column,
                        view: grid,
                        cell: false
                    };
 
                    if (row = ST.future.Row.getItemAt(grid, row)) {
                        info.cell = row.cells[colIndex].el;
                    }
 
                    return info;
                },
 
                scrollTo: function (grid, rowIndex, colIndex, callback) {
                    var me = this,
                        scroller = grid.getScrollable(),
                        record = grid.getStore().getAt(rowIndex),
                        row = ST.future.Row.getItemAt(grid, rowIndex),
                        scrollCell = colIndex !== null,
                        hasPromise = grid.mapToViewIndex,
                        cell, cellReady, rowReady;
 
                    cellReady = function (cell) {
                        if (hasPromise) {
                            scroller.scrollIntoView(cell, true, false).then(function () {
                                callback(cell);
                            })
                        } else {
                            scroller.on('scrollend', function () {
                                callback(cell);
                            }, me, {
                                single: true
                            });
                            // scroll the cell into view, and make sure to allow horizontal scrolling!!!
                            scroller.scrollIntoView(cell, true, false);
                        }
                    }
 
                    rowReady = function (row) {
                        if (scrollCell) {
                            cell = row.cells[colIndex].el;
                            // if the cell isn't in view, we need to scroll it before anything else can occur
                            if (!scroller.isInView(cell).x) {
                                cellReady(cell);
                            } else {
                                callback(cell);
                            }          
                        } else { // scrolling a row
                            callback(row.el);
                        }
                    }
                    // if row is in view, we know that both the row and cell are available, 
                    // there's no need to scroll and we can execute the callback immediately
                    if (row && scroller.isInView(row.el).y) {
                        rowReady(row);  
                    } 
                    // if row isn't in view, we need to wait for scrollend to fire
                    else {
                        if (hasPromise) {
                            grid.scrollToRecord(record, false).then(function (opts) {
                                var item = opts && opts.item || grid.getItem(record);
                                rowReady(item);
                            });
                        } else {
                            scroller.on('scrollend', function() {
                                rowReady(grid.getItem(record));
                            }, me, {
                                single: true
                            });
                            // will scroll record into view
                            grid.scrollToRecord(record, false, true);
                        }          
                    }            
                }
            }
        }
    }
}); // Ext.future.Grid
 
/**
 * Returns a {@link ST.future.Grid future Grid} used to queue operations for
 * when that `Ext.grid.Panel` becomes available.
 * @param {String} locator See {@link ST.Locator} for supported syntax.
 * @param {Number} [timeout] The maximum time (in milliseconds) to wait for the dataview.
 * @return {ST.future.Grid}
 * @method grid
 * @member ST
 */
 
ST.future.resolveToolkitMethods = function () {
    var target = ST.isClassic ? 'classic' : (ST.isTouch ? 'touch' : 'modern'),
        classes = ST.future.classes,
        len = classes.length,
        owner, i, cls, proto, config, targetConfig, name;
 
    for (i=0; i<len; i++) {
        config = null;
        owner = null;
        cls = classes[i];
        proto = cls.prototype;
 
        if (cls.toolkitConfig) {
            owner = cls;
            config = cls.toolkitConfig;
        }
 
        if (proto.toolkitConfig) {
            owner = proto;
            config = proto.toolkitConfig;
        }
        // we found the config
        if (config) {
            // retrieve the items we want to apply by target (e.g., 'classic', 'modern', 'touch')
            targetConfig = config[target];
            // if touch doesn't have an object defined, use modern
            if (!targetConfig && target==='touch') {
                targetConfig = config['modern'];
            }
            // sanity check
            if (typeof targetConfig === 'object') {
                // we got our object of items to apply; loop over it and apply away
                for (name in targetConfig) {
                    owner[name] = targetConfig[name];
                }
            }
            // we're done, let's cleanup
            delete owner.toolkitConfig;
        }
    }
};
 
// add callback to ST.ready.on to run any logic needed before everything else gets going
if (ST.ready) {
    ST.ready.on(ST.future.resolveToolkitMethods);
}