/**
 * @class ST.locator.Strategy
 * This class is the default locator strategy. This can also be used as a base class for
 * other locator strategies. To register custom locator strategies, see
 * {@link ST#addLocatorStrategy} or {@link ST#setLocatorStrategies}.
 */
ST.locator.Strategy = ST.define({
    // TODO refactor target hunting methods to return targets found, not pass around arrays
    ignoreElIdRe: /^ext-(?:gen|element)(?:\d+)$/,
    validIdRe: /^[a-z_][a-z0-9\-_]*$/i,
    autoGenIdRe: /^ext-/,
    includeFullLocator: ST.apply(ST.apply({},
        ST.event.Event.keyEvents),
        ST.event.Event.focusEvents),
 
    constructor: function (config) {
        var me = this;
 
        ST.apply(me, config);
 
        me.locate = me._initter;  // hook the first call to locate()
    },
 
    /**
     * Initializes this instance. This method is called immediately prior to the first
     * call to `locate`. This is done in a deferred manner to ensure that any necessary
     * code has been loaded (such as Ext JS).
     *
     * @since 1.0.1
     * @protected
     */
    init: function () {
        var me = this,
            x = ST.Ext,
            cssIgnore = me.cssIgnore,
            cssIgnorePat;
 
        // Copy off the prototype
        me.cssIgnore = cssIgnore = ST.apply({}, cssIgnore);
 
        x = x && x.baseCSSPrefix;
        cssIgnorePat = me.getCssIgnorePatterns(x);
 
        if (cssIgnorePat.length) {
            me.cssIgnoreRe = new RegExp(cssIgnorePat.join('|'));
        }
 
        if (x) {
            ST.apply(cssIgnore, ST.Array.toMap([
                x + 'body',
 
                x + 'box-item',
                x + 'box-target',
 
                x + 'btn-inner',
                x + 'btn-wrap',
 
                x + 'component',
                x + 'fa',
                x + 'fit-item',
                x + 'form-field',
                x + 'grid-cell-inner',
                x + 'noicon'
            ]));
        }
    },
 
    _initter: function () {
        var me = this;
 
        delete me.locate;
 
        me.init();
 
        return me.locate.apply(me, arguments);
    },
 
    /**
     * This method should append target locators to the `targets` array based on the
     * provided `el`. Each identified target should be appended to the `targets` array
     * (e.g., using `push()`).
     *
     * Because a locator can describe the passed `el` or a parent node, results added to
     * the `targets` array should be an array consisting of the element and its locator.
     * For example:
     *
     *      if (el.id) {
     *          targets.push([ el, '@' + el.id ]);
     *      }
     *
     * @param {HTMLElement} el The element for which to generate target locator(s).
     * @param {Array[]} targets The array to which to append targets and their locator(s)
     * as an array of `[ el, locator ]`.
     * @param {Object} ev 
     * @param {Boolean} noComponentLocators if `true` then don't return locators which locate Ext Components
     * @return {Boolean} Returns `true` if any `targets` were generated.
     * @method locate
     */
    locate: function (el, targets, ev, noComponentLocators) {
        var me = this,
            ExtJS = ST.Ext,
            good = false,
            c, cmp, fly;
 
        if (ExtJS && ExtJS.ComponentQuery && !noComponentLocators) {
            fly = ST.fly(el);
            cmp = fly && fly.getComponent();
 
            for (= cmp; c; c = c.getRefOwner && c.getRefOwner()) {
                if (!me.ignoreCmp(c)) {
                    if (me.getCQ(c, el, targets, ev)) {
                        me.processCustomBuilder(c, el, targets, ev);
                        good = true;
                    }
                    break;
                }
            }
        }
 
        if (me.getAtPath(el, targets)) {
            good = true;
        }
 
        if (me.getXPath(el, targets)) {
            good = true;
        }
 
 
        return !!good;
    },
 
    getAtPath: function (el, targets) {
        if (!el) {
            el = document.body;
        }
 
        var me = this,
            good = false,
            path = [],
            stopper = (el.ownerDocument || el).body,
            count, sibling, t, tag;
 
        for (= el; t; t = t.parentNode) {
            if (== stopper) {
                path.unshift('@');
                good = true;
                break;
            }
            if (t.id && t.id.indexOf('/') < 0 && !me.ignoreElIdRe.test(t.id)) {
                path.unshift('@' + t.id);
                good = true;
                break;
            }
 
            for (count = 1, sibling = t; (sibling = sibling.previousSibling); ) {
                if (sibling.tagName == t.tagName) {
                    ++count;
                }
            }
 
            tag = t.tagName && t.tagName.toLowerCase();
            if (tag) {
                if (count < 2) {
                    path.unshift(tag);
                } else {
                    path.unshift(tag + '[' + count + ']');
                }
            } else if (t.window == t) { // must use == for IE8 (from Ext.dom.Element)
                break;
            }
        }
 
        if (targets && good) {
            var tmpel = ST.Locator.find(path[0]);
            if (tmpel && tmpel == el) {
                targets.push([t, path[0]]);
            }
 
            if (path.length > 1) {
                targets.push([el, path.join('/')]);
            }
        }
 
        return good;
    },
 
    getXPath: function (el, targets) {
        if (!el) {
            el = document.body;
        }
        if (el.$className) {
            el = el.dom;
        }
 
        var fly = ST.fly(el),
            xpath = fly.getXPath(),
            good = !!xpath;
        
        if (targets && good) {
            targets.push([el, xpath]);
        }
 
        return good;
    },
 
 
    /**
     * Generates a set of ComponentQuery candidates for the given Component. The generated
     * CQ selectors are "shallow" in that they do not describe the containment hierarchy
     * of the component.
     *
     * @param {Ext.Component} cmp 
     * @param {HTMLElement} el The actual element to target. If this parameter is not
     * `null` this method may include an additional DOM query on the generated selectors
     * separated by "=>" (a "Composite Query").
     * @param {Array[]} targets The array to which to append targets and their locator(s)
     * as an array of `[ el, locator ]`.
     * @param {Object} ev 
     * @return {Boolean} Returns `true` if any `targets` were generated.
     * @since 1.0.1
     */
    getCQ: function (cmp, el, targets, ev) {
        var me = this,
            configList = me.configList,
            len = configList.length,
            good = false,
            n = targets && targets.length,
            i, item, k, sel, xtype;
 
        for (= 0; i < len; ++i) {
            if (me.getCQForProperty(cmp, configList[i], targets)) {
                good = true;
            }
        }
 
        if (!good) {
            xtype = me.getXType(cmp);
 
            if (xtype) {
                if (targets) {
                    targets.push([cmp.el, xtype]);
                }
 
                good = true;
            }
        }
 
        if (targets && el && n < (= targets.length)) {
            sel = me.getItemSelector(cmp, el, ev);
 
            if (sel) {
                item = sel[0];
                sel = ' => ' + sel[1];
 
                for (; n < k; ++n) {
                    targets[n][0] = item;
                    targets[n][1] += sel;
                }
            }
        }
 
        return good;
    },
 
    /**
     * Generates a ComponentQuery selector for the given Component using the specified
     * config property. The selector is "shallow" in that they do not describe the
     * containment hierarchy of the component.
     *
     * The supported properties are listed in the `configList` array.
     *
     * @param {Ext.Component} cmp 
     * @param {String} prop The property to use in the generated selector.
     * @param {Array[]} targets The array to which to append targets and their locator(s)
     * as an array of `[ el, locator ]`.
     * @return {Boolean} Returns `true` if any `targets` were generated.
     * @since 1.0.1
     */
    getCQForProperty: function (cmp, prop, targets) {
        var extractor = this.extractors[prop],
            good = false;
 
        if (extractor) {
            good = extractor.call(this, cmp, targets);
        }
 
        return good;
    },
 
    getItemSelector: function (cmp, el, ev) {
        var view = ST.fly(el).getComponent(),
            item = view.findItemByChild && view.findItemByChild(el),
            cell, col, colId, recId, ret, idx;
 
        if (item) {
            recId = item.getAttribute('data-recordindex');
            if (recId) {
                ret = [item, '[data-recordindex=' + JSON.stringify(recId) + ']'];
 
                for (cell = el; cell && cell !== item; cell = cell.parentNode) {
                    colId = cell.getAttribute('data-columnid');
                    col = colId && ST.Ext.getCmp(colId);
 
                    if (col) {
                        if (!col.autoGenId) {
                            ret[0] = cell;
                            ret[1] += ' [data-columnid=' + JSON.stringify(colId) + ']';
                        }
                        break;
                    }
                }
            } else {
                // try to find item by index...not perfect, but something
                idx = el.tagName.toLowerCase() === 'li' && view.indexOf && view.indexOf(el);
 
                if (typeof idx !== 'undefined' && idx >= 0) {
                    ret = [item, 'li:nth-child(' + (idx + 1) + ')'];
                }
            }
        } else if (ev && this.includeFullLocator[ev.type]) {
            var matches = cmp.el.dom.querySelectorAll(el.tagName);
            if (matches && matches.length === 1) {
                ret = [el, el.tagName.toLowerCase()];
            }
        }
 
        return ret;
    },
 
    extractors: {
        iconCls: function (cmp, targets) {
            var iconCls = cmp.iconCls;
 
            if (iconCls) {
                if (targets) {
                    targets.push([
                        cmp.el.dom,
                        this.getXType(cmp) + '[iconCls="' + iconCls + '"]'
                    ]);
                }
 
                return 1;
            }
 
            return 0;
        },
 
        id: function (cmp, targets) {
            var me = this,
                id = me.getCmpId(cmp),
                parent;
 
            if (cmp.autoGenId || !me.validIdRe.test(id) || me.autoGenIdRe.test(id)) {
                return 0;
            }
 
            // We can still have an autoGenId on a parent that is then used to produce
            // this id (panel-1010_header). So start with the parent and see if its id
            // is a prefix of cmp's id and if it is an autoGenId.
            for (parent = cmp; (parent = parent.getRefOwner && parent.getRefOwner()); ) {
                if (!ST.String.startsWith(id, me.getCmpId(parent))) {
                    break;
                }
 
                if (parent.autoGenId) {
                    return 0;
                }
            }
 
            if (targets) {
                targets.push([
                    cmp.el.dom,
                    '#' + id
                ]);
            }
 
            return 1;
        }
    },
 
    /**
     * @property {Object} classIgnore
     * Property names are the Ext JS classes to ignore.
     */
    classIgnore: {
        //
    },
 
    /**
     * @property {Object} cmpIgnore
     * Property names are the component xtypes to ignore. Values are either the xtype
     * of the parent if the component should only be ignored when inside this type of
     * parent, or `true` to always ignore.
     */
    cmpIgnore: {
        gridview: 'grid',
        tableview: 'grid',
        treeview: 'tree'
    },
 
    /**
     * @property {String[]} configList
     * The list of config properties used to identify components in order of priority.
     * @since 1.0.1
     */
    configList: [
        'id',
        'stateId',
        'reference',
        'itemId',
        'fieldLabel', //Moving fieldLabel to the top of generic identifier list to give preference. Fix for ORION-2300
        'boxLabel',
        'name',
        'iconCls',
        'text',
        'label'
    ],
 
    /**
     * @property {Object} cssIgnore
     * Property names are the CSS class names to ignore.
     */
    cssIgnore: {
        fa: 1,  // FontAwesome
 
        // Some old framework bugs rendered null/undefined into class attribute
        'null': 1,
        'undefined': 1
    },
 
    /**
     * Returns the id of the given Component.
     * @param {Ext.Component} cmp 
     * @return {String} 
     */
    getCmpId: function (cmp) {
        return cmp.getId ? cmp.getId() : cmp.id;
    },
 
    /**
     * Returns an array of `RegExp` patterns that describe CSS classes to be ignored.
     * @param {String} baseCSSPrefix The CSS prefix for Ext JS (typically "x-").
     * @return {String[]} 
     * @since 1.0.1
     * @protected
     */
    getCssIgnorePatterns: function (baseCSSPrefix) {
        var x = baseCSSPrefix;
 
        if (!x) {
            return [];
        }
 
        x = '^' + x;
 
        return [
            x + 'noborder'
        ];
    },
 
    /**
     * Returns the `xtype` of the given Component. If the component has multiple xtypes,
     * the primary is returned.
     *
     * @param {Ext.Component} cmp 
     * @return {String} 
     * @since 1.0.1
     */
    getXType: function (cmp) {
        var xtype = cmp.getXType && cmp.getXType();
 
        if (!xtype) {
            xtype = cmp.xtype || (cmp.xtypes && cmp.xtypes[0]);
        }
 
        xtype = cmp.$reactorComponentName || xtype;
 
        return xtype;
    },
 
    /**
     * Returns `true` if the given CSS class should be ignored.
     * @param {String} cls 
     * @return {Boolean} 
     * @protected
     */
    ignoreCls: function (cls) {
        var cssIgnoreRe = this.cssIgnoreRe;
 
        return this.cssIgnore[cls] || (cssIgnoreRe && cssIgnoreRe.test(cls));
    },
 
    ignoreCmp: function (cmp) {
        var me = this,
            xtype = me.getXType(cmp),
            ignore = me.cmpIgnore[xtype],
            parent, parentXType;
 
        if (ignore) {
            if (typeof ignore !== 'string') {
                return true;
            }
 
            parent = cmp.getRefOwner && cmp.getRefOwner();
            if (parent) {
                if (typeof parent.isXType === 'function') {
                    if (parent.isXType(ignore)) {
                        return true;
                    }
                } else {
                    parentXType = me.getXType(parent);
 
                    if (parentXType === ignore) {
                        return true;
                    }
                }
            }
        }
 
        if (me.classIgnore[cmp.$className]) {
            return true;
        }
 
        return false;
    },
 
    /**
     * Returns the array of CSS classes given the `className` (space-separated classes).
     * The ignored classes have been removed from the array.
     *
     * @param {String} cls 
     * @return {String[]} 
     * @since 1.0.1
     * @protected
     */
    splitCls: function (cls) {
        var array = ST.String.split(ST.String.trim(cls)),
            len = array.length,
            c, i, ret;
 
        for (= 0; i < len; ++i) {
            c = array[i];
 
            if (&& !this.ignoreCls(c)) {
                (ret || (ret = [])).push(c);
            }
        }
 
        return ret;
    },
 
    customBuilders: {
        datePicker: function (cmp, el, targets, ev) {
            var me = this,
                node = el,
                classes = el.classList,
                len = targets.length,
                isItem = false,
                isField = !!cmp.pickerField, // could be standalone datepicker
                lt423 = Ext.versions.extjs.isLessThan('4.2.3'),
                is5 = Ext.versions.extjs.gtEq('5'),
                id, postLen, i;
 
            if (classes.contains('x-datepicker-cell')) {
                isItem = true;
            } else if (classes.contains('x-datepicker-date')) {
                node = node.parentNode;
                isItem = true;
            } else {
                el = cmp.el;
            }
            
            // if this is a picker attached to a field, make the field the root query object
            if (isField) {
                cmp = cmp.pickerField;
            }
 
            me.getCQ(cmp, el, targets, ev);
            postLen = targets.length;
 
            if (isItem) {
                id = node.id.replace(/^datefield|datepicker-[0-9]*-/, '');
 
                for (i=len; i<postLen; i++) {
                    if (isField) {
                        if (lt423) {
                            targets[i][1] = 'datepicker[hidden=false]';
                        } else {
                            targets[i][1] += ' datepicker';
                        }
                    }
 
                    targets[i][0] = el;
 
                    if (lt423) {
                        targets[i][1] += ' => ' + '[title="' + node.title + '"] a';
                    } else if (is5) {
                        targets[i][1] += ' => ' + '[aria-label="' + node.getAttribute('aria-label') + '"] div';
                    }
                }
            } else if (isField) {
                for (i=len; i<postLen; i++) {
                    targets[i][0] = el;
 
                    if (lt423) {
                        targets[i][1] = 'datepicker[hidden=false]';
                    } else {
                        targets[i][1] += ' datepicker';
                    }
                }
            }    
 
            me.promoteLocators(targets, len, postLen-len);
        },
 
        timePicker: function (cmp, el, targets, ev) {
            var me = this,
                len = targets.length,
                postLen, i;
 
            me.getCQ(cmp.pickerField, el, targets, ev);
            postLen = targets.length;
 
            for (i=len; i<postLen; i++) {
                targets[i][0] = el;
 
                if (Ext.versions.extjs.isLessThan('4.2.3')) {
                    targets[i][1] = targets[i][1].replace(/.*=>/, 'timepicker[hidden=false] =>');
                } else {
                    targets[i][1] = targets[i][1].replace('=>', 'timepicker =>');
                }
 
                // if we're scrolling, the scroll is happening on the boundlist, so target its element
                if (ev && ev.type === 'scroll' && targets[i][1].search(/=>/) === -1) {
                    if (Ext.versions.extjs.isLessThan('4.2.3')) {
                        targets[i][1] = ' timepicker[hidden=false]';
                    } else {
                        targets[i][1] += ' timepicker';
                    }
                }
            }
 
            me.promoteLocators(targets, len, postLen-len);
        },
 
        boundList: function (cmp, el, targets, ev) {
            var me = this,
                len = targets.length,
                postLen, i, boundlists, index;
 
            var owner = cmp.up('itemselectorfield') || cmp.up('multiselect');
 
            if (owner) {
                me.getCQ(owner, el, targets, ev);
                boundlists = owner.query('boundlist');
                if (boundlists.length > 0) {
                    index = boundlists.indexOf(cmp);
                }
            } else if (cmp.pickerField) {
                me.getCQ(cmp.pickerField, el, targets, ev);
            } else {
                return false;
            }
            postLen = targets.length;
 
            for (i=len; i<postLen; i++) {
                targets[i][0] = el;
 
                if (typeof boundlists !=='undefined' && boundlists.length > 1) {
                    if (index == 0) {
                        targets[i][1] = targets[i][1].replace('=>', 'boundlist:first =>');
                    } else {
                        targets[i][1] = targets[i][1].replace('=>', 'boundlist:last =>');
                    }
                } else if (!Ext.versions.extjs.isLessThan('4.2.3') || owner) {
                    targets[i][1] = targets[i][1].replace('=>', 'boundlist =>');
                } else {
                    targets[i][1] = targets[i][1].replace(/.*=>/, 'boundlist[hidden=false][pickerField] =>');
                }
 
                // if we're scrolling, the scroll is happening on the boundlist, so target its element
                if (ev && ev.type === 'scroll' && targets[i][1].search(/=>/) === -1) {
                    if (typeof boundlists !=='undefined' && boundlists.length > 1) {
                        if (index == 0) {
                            targets[i][1] += ' boundlist:first';
                        } else {
                            targets[i][1] += ' boundlist:last';
                        }
                    } else if (!Ext.versions.extjs.isLessThan('4.2.3') || owner) {
                        targets[i][1] += ' boundlist';
                    } else {
                        targets[i][1] = ' boundlist[hidden=false][pickerField]';
                    }
                }
            }
 
            me.promoteLocators(targets, len, postLen-len);
        },
 
        tablePanel: function (cmp, el, targets, ev) { 
            var me = this,
                len = targets.length,
                postLen, i;
            
            // if a scroll is happening on a grid, target the dataview of the grid
            if (ev && ev.type === 'scroll') { 
                me.getCQ(cmp, el, targets, ev);
                postLen = targets.length;
 
                for (= len; i < postLen; i++) {
                    targets[i][1] += ' dataview';
                }
 
                me.promoteLocators(targets, len, postLen - len);
            }
        },
 
        component: function (cmp, el, targets, ev) { 
            var me = this,
                len = targets.length,
                postLen, listOwner, dvOwner;
 
            if (ev && ev.type === 'scroll' && Ext.versions.modern) { 
                listOwner = cmp.up('list');
                dvOwner = cmp.up('dataview');
                
                // since modern lists/grids may have components as items (or as items of items!),
                // the scroll target might get lost; so we'll sniff for it and standardize to the view if found
                if (listOwner || dvOwner) { 
                    cmp = listOwner || dvOwner;
                    me.getCQ(cmp, el, targets, ev);
                    postLen = targets.length;
 
                    me.promoteLocators(targets, len, postLen - len);
                }
            }
        },
 
        gridColumn: function (cmp, el, targets, ev) {
            var me = this,
                len = targets.length,
                classicTriggerCls = 'x-column-header-trigger',
                modernTriggerCls = 'x-trigger-el',
                triggerCls = [classicTriggerCls, modernTriggerCls],
                classicCheckboxCls = 'x-column-header-checkbox',
                modernCheckboxCls = 'x-checkbox-el',
                checkboxCls = [classicCheckboxCls, modernCheckboxCls],
                isTrigger = false,
                isCheckbox = false,
                isCheckerHd = cmp.isCheckerHd, // classic selection checkbox
                isModernSelectionCheckbox = cmp.isXType('selectioncolumn'), // modern selection checkbox
                isSelectionCheckbox = isCheckerHd || isModernSelectionCheckbox,
                isModern = Ext.versions.modern,
                owner = cmp.up('tablepanel') || cmp.up('grid'), // support classic and modern
                postLen, i, selector, type;
            
            Ext.Array.forEach(checkboxCls, function (cls) {
                if (el.classList.contains(cls)) { 
                    isCheckbox = true;
                }
            });
 
            Ext.Array.forEach(triggerCls, function (cls) {
                if (el.classList.contains(cls)) {
                    isTrigger = true;
                }
            });
 
            if (owner) { 
                me.getCQ(owner, el, targets, ev);
                postLen = targets.length;
 
                for (= len; i < postLen; i++) {
                    targets[i][0] = el;
                    selector = cmp.dataIndex || cmp.getDataIndex && cmp.getDataIndex();
                    type = 'dataIndex';
 
                    if (!selector || isSelectionCheckbox) {
                        // if a checkbox header, we want to use it instead of the text
                        if (isCheckerHd) {
                            selector = true;
                            type = 'isCheckerHd';
                        } else if (isModernSelectionCheckbox) { 
                            selector = true;
                            type = 'rendered';
                        } else {
                            selector = cmp.text || cmp.getText && cmp.getText();
                            type = 'text';
                        }
                    }
 
                    if (selector) {
                        targets[i][1] += ' ' + me.getXType(cmp) + '[' + type + '="' + selector + '"]';
                        // if it's a trigger or a checkbox header, we want to use the actual element since the header
                        // could have extra content in it (text, etc)
                        if (isTrigger) {
                            targets[i][1] += ' => .' + (isModern ? modernTriggerCls : classicTriggerCls);
                        } else if (isModernSelectionCheckbox) { 
                            targets[i][1] += ' => .x-checkbox-el'; // modern
                        } else if (isCheckbox) {
                            targets[i][1] += ' => .' + (isModern ? modernCheckboxCls : classicCheckboxCls); // modern, classic 6.2.x +
                        } else if (isCheckerHd) {
                            targets[i][1] += ' => .x-column-header-text'; // < classic 6.2.x
                        }
                    }
                }
 
                me.promoteLocators(targets, len, postLen - len);
            }
        },
        
        menuItem: function (cmp, el, targets, ev) {
 
            //filter for textfield does not perform pointer or mouse event 
            //so it doesn't give back child element so here manually we move to child component and 
            //put repeat check so that it won't wind up in infinte loop.
            if (ev.type === 'blur' && ev.relatedTarget) {
                if (ev.relatedTarget.localName === 'input' && !ev.repeat && cmp.down) {
                    cmp = cmp.down().down();
                    ev.repeat = true;
                }
            }
            var me = this,
                len = targets.length,
                buttonOwner = cmp.up('button'),
                headerOwner = cmp.up('headercontainer'),
                prereqs = [],
                postLen, i, fn, menuFn, itemFn;
            
            // build out a component query for the menu; could be nested, so recursively call this as needed
            // so that we get at the right things
            menuFn = function (item, locator) {
                var partial, owner, text;
 
                if (item.isXType('menuitem')) {
                    text = item.getText ? item.getText() : item.text;
                    partial = me.getXType(item) + '[text="' + text + '"]';
 
                    if (locator) {
                        partial += ' > ';
                    } else {
                        locator = '';
                    }
 
 
                    locator = menuFn(item.up('menu'), partial + locator);
                } else if (item.isXType('menu')) {
                    locator = 'menu > ' + locator;
                    owner = item.up('menuitem');
 
                    if (owner) {
                        locator = menuFn(owner, locator);
                    }                    
                }
 
                return locator;
            }
 
            if (buttonOwner) {
                me.getCQ(buttonOwner, el, targets, ev);
                postLen = targets.length;
 
                for (i=len; i<postLen; i++) {
                    targets[i][0] = el;
                    targets[i][1] += ' ' + menuFn(cmp);
                }
            } else if (headerOwner) {
                me.getCQ(cmp.up('grid'), el, targets, ev);
                postLen = targets.length;
 
                for (i=len; i<postLen; i++) {
                    targets[i][0] = el;
 
                    if (Ext.versions.modern) { 
                        targets[i][1] = menuFn(cmp).replace(/^menu/, 'menu[floated=true][hidden=false]');
                    } else if (Ext.versions.extjs.isLessThan('6.2.0')) {
                        targets[i][1] = menuFn(cmp).replace(/^menu/, 'menu[floating=true][hidden=false]');
                    } else {
                        targets[i][1] += ' ' + menuFn(cmp);
                    }
                }
            }
 
            if (ev && (ev.type === 'mousedown' || ev.type === 'pointerdown' || (ev.type === 'blur' && ev.repeat === true))) {
                itemFn = function (_cmp) {
                    var owner = _cmp.up('menuitem'),
                        parent, match;
 
                    if (owner) {
                        match = owner;
                    } else {
                        // in earlier versions of Ext JS, menus didn't have menu items as ancestors
                        // so we need to be a bit more prescriptive in searching for the correct ancestor
                        parent = _cmp.up('menu');
                        owner = parent.up('menu');
 
                        if (owner) {
                            // go through items collection and see if menu matches
                            owner.items.each(function (item) {
                                if (item.menu && item.menu === parent) {
                                    match = item;
                                }
                            });
                        }
                    }
 
                    return match;
                }
 
                fn = function (_cmp) {
                    var itemOwner = itemFn(_cmp), 
                        ownerTargets = [],
                        prereq, x;
 
                    if (itemOwner) {
                        me.locate(itemOwner.el, ownerTargets, ev);
                        prereq = {
                            type: 'menuitemover',
                            target: ownerTargets[0][1],
                            targets: []
                        };
 
                        for (x=0; x<ownerTargets.length; x++) {
                            prereq.targets.push({
                                target: ownerTargets[x][1]
                            });
                        }
                        
                        prereqs.push(prereq);
 
                        // call recursively until we reach the top
                        fn(itemOwner);
                    }
                };
                
                fn(cmp);
 
                if (prereqs.length) {
                    ev.prereq = prereqs.reverse();
                }
            }
 
            me.promoteLocators(targets, len, postLen-len);
        },
 
        spinnerField: function (cmp, el, targets, ev) {
            var me = this,
                node = el,
                len = targets.length,
                isTrigger = false,
                triggerCls, postLen, i, cls;
 
            triggerCls = el.className.match(/x-form-spinner-up|x-form-spinner-down/);
 
            if (triggerCls && triggerCls.length) {
                isTrigger = true;
                cls = triggerCls[0];
            }
 
            if (isTrigger) {
                me.getCQ(cmp, el, targets, ev);
                postLen = targets.length;
 
                for (= len; i < postLen; i++) {
                    targets[i][0] = node;
                    targets[i][1] += ' => ' + '[class*="' + cls + '"]';
                }
            }
 
            if (postLen !== 'undefined') {
                me.promoteLocators(targets, len, postLen - len);
            }
        },
 
        colorPicker: function (cmp, el, targets, ev) {
            var me = this,
                node = el,
                len = targets.length,
                isItem = false,
                isButton = false,
                lt423 = Ext.versions.extjs.isLessThan('4.2.3'),
                cls, postLen, i;
 
            if (el.className.match(/x-color-picker-item-inner/)) {
                isItem = true;
                node = el.parentNode;
            } else if (el.className.match(/x-color-picker-item/)) {
                isItem = true;
            }
 
            if (cmp.up('button')) {
                cmp = cmp.up('button');
                el = cmp.el;
                isButton = true;
            }
 
            me.getCQ(cmp, el, targets, ev);
            postLen = targets.length;
 
            if (isItem) {
                cls = node.className.match(/color-[0-9A-Z]{6}/);
 
                if (cls) {
                    for (= len; i < postLen; i++) {
                        targets[i][0] = node;
 
                        if (isButton) {
                            targets[i][1] += ' colorpicker';
                        } else {
                            targets[i][1] = 'colorpicker[rendered=true][hidden=false]';
                        }
 
                        if (lt423) {
                            targets[i][1] += ' => ' + '[class*="' + cls[0] + '"]';
                        } else {
                            targets[i][1] += ' => ' + '[class*="' + cls[0] + '"]';
                        }
                    }
                }
            }
 
            me.promoteLocators(targets, len, postLen - len);
        },
 
        monthPicker: function (cmp, el, targets, ev) {
            var len = targets.length,
                i;
 
            for (= 0; i < len; i++) {
                targets[i][1] = targets[i][1].replace('monthpicker', 'monthpicker[hidden=false]');
            }
        },
 
        button: function (cmp, el, targets, ev) {
            var me = this,
                len = targets.length,
                lt423 = Ext.versions.extjs.isLessThan('4.2.3'),
                lt5 = Ext.versions.extjs.isLessThan('5'),
                postLen, i, parent, isField, fld, pos, isTodayBtn, isMonthBtn, toolbarCls;
 
            // only proceed if this is a button within a datepicker
            if (cmp.up('monthpicker')) {
                parent = cmp.up('monthpicker');
 
                if (lt5) {
                    var stel = ST.get(parent.container.dom)
                    var found = stel && stel.getComponent();
 
                    if (found) {
                        isField = !!found.pickerField;
                        fld = found.pickerField;
                    }
                } else {
                    isField = !!cmp.up('datepicker').pickerField;
                    fld = cmp.up('datepicker').pickerField;
                }
 
                pos = cmp === parent.okBtn ? 1 : parent.cancelBtn ? 2 : 1;
 
                me.getCQ(cmp, el, targets, ev);
 
                postLen = targets.length;
 
                for (= len; i < postLen; i++) {
                    if (isField) {
                        targets[i][1] = 'monthpicker[hidden=false] => .x-monthpicker-buttons .x-btn:nth-child(' + pos + ')';
                    } else {
                        targets[i][1] += ' => .x-monthpicker-buttons .x-btn:nth-child(' + pos + ')';
                    }
                }
            } else if (cmp.up('datepicker')) {
                parent = cmp.up('datepicker');
                isField = !!cmp.up('datepicker').pickerField;
                fld = cmp.up('datepicker').pickerField; // could be standalone datepicker
 
                if (isField && !lt423) {
                    me.getCQ(fld, fld.el, targets, ev);
                } else {
                    me.getCQ(cmp, el, targets, ev);
                }
 
                postLen = targets.length;
 
                for (= len; i < postLen; i++) {
                    if (lt423) {
                        isTodayBtn = fld.picker.todayBtn === cmp;
                        isMonthBtn = fld.picker.monthBtn === cmp;
                        toolbarCls = isTodayBtn ? '.x-datepicker-footer' : '.x-datepicker-header';
 
                        targets[i][1] = 'datepicker[hidden=false] => ' + toolbarCls + ' .x-btn';
                    } else {
                        if (isField) {
                            // switch el to actual button since we're rooting to field
                            targets[i][0] = el;
                            targets[i][1] += ' datepicker button[text="' + cmp.text + '"]';
                        } else {
                            targets[i][1] = 'datepicker ' + targets[i][1];
                        }
                    }
                }
            }
 
            if (typeof postLen !== 'undefined') {
                me.promoteLocators(targets, len, postLen - len);
            }
        },
 
        radio: function (cmp, el, targets, ev) {
            var me = this,
                len = targets.length,
                owner = cmp.up('radiogroup'),
                postLen, i;
            
            // If the radio button belongs to a radio group, get a component query for that group
            if (owner) { 
                me.getCQ(owner, el, targets, ev);
                postLen = targets.length;
 
                if (postLen > len) {
                    for (= len; i < postLen; i++) {
                        targets[i][0] = el;
                        
                        // Generate a combined radio group and radio button locator, using first radio locator
                        targets[i][1] += ' ' + targets[0][1];
                    }
 
                    me.promoteLocators(targets, len, postLen - len);
                }
            }
        },
 
        checkboxField: function (cmp, el, targets, ev) {
            var me = this,
                len = targets.length,
                owner = cmp.up('checkboxgroup'),
                postLen, i;
            
            // If the checkbox belongs to a checkbox group, get a component query for that group
            if (owner) { 
                me.getCQ(owner, el, targets, ev);
                postLen = targets.length;
 
                if (postLen > len) {
                    for (= len; i < postLen; i++) {
                        targets[i][0] = el;
                        
                        // Generate a combined checkbox group and checkbox locator, using first checkbox locator
                        targets[i][1] += ' ' + targets[0][1];
                    }
 
                    me.promoteLocators(targets, len, postLen - len);
                }
            }
        }
    },
 
    promoteLocators: function (targets, pos, count) {
        var map = {},
            spliced = targets.splice(pos, count).reverse(),
            i, key, x;
 
        for (= 0; i < spliced.length; i++) {
            targets.unshift(spliced[i]);
        }
 
        for (= targets.length - 1; x >= 0; --x) {
            key = targets[x][1];
 
            if (map[key]) {
                // get rid of array item, it's a dupe
                targets.splice(x, 1);
            } else {
                map[key] = true;
            }
        }
    },
 
    findCustomBuilder: function (cmp) {
        // Default to use "getXTypes", or failing that use "xtypesChain" - copy it so we don't mutate the original
        var chain = cmp.getXTypes ? cmp.getXTypes().split('/') : cmp.xtypesChain.slice(),
            xtypeList = chain.reverse(), // reverse the xytype list so we get most specific first
            matchers = ['monthpicker', 'datepicker', 'timepicker', 'colorpicker', 'tablepanel', 'component', 'boundlist', 'spinnerfield', 'gridcolumn', 'menuitem', 'button', 'radio', 'checkboxfield'],
            len = matchers.length,
            xtype, i;
 
        for (= 0; i < len; i++) {
            if (Ext.Array.contains(matchers, xtypeList[i])) {
                xtype = xtypeList[i];
                break;
            }
        }
        return xtype;
    },
 
    processCustomBuilder: function (cmp) {
        var me = this,
            xtype = me.findCustomBuilder(cmp);
 
        if (xtype) {
            switch (xtype) {
                case 'button':
                    me.customBuilders.button.apply(me, arguments);
                    break;
                case 'spinnerfield':
                    me.customBuilders.spinnerField.apply(me, arguments);
                    break;
                case 'monthpicker':
                    me.customBuilders.monthPicker.apply(me, arguments);
                    break;
                case 'datepicker':
                    me.customBuilders.datePicker.apply(me, arguments);
                    break;
                case 'timepicker':
                    me.customBuilders.timePicker.apply(me, arguments);
                    break;
                case 'colorpicker':
                    me.customBuilders.colorPicker.apply(me, arguments);
                    break;
                case 'boundlist':
                    me.customBuilders.boundList.apply(me, arguments);
                    break;
                case 'gridcolumn':
                    me.customBuilders.gridColumn.apply(me, arguments);
                    break;
                case 'menuitem':
                    me.customBuilders.menuItem.apply(me, arguments);
                    break;
                case 'tablepanel':
                    me.customBuilders.tablePanel.apply(me, arguments);
                    break;
                case 'radio':
                    me.customBuilders.radio.apply(me, arguments);
                    break;
                case 'checkboxfield':
                    me.customBuilders.checkboxField.apply(me, arguments);
                    break;
                case 'component':
                    me.customBuilders.component.apply(me, arguments);
                    break;
            }
        }
    }
}, function (Strategy) {
    function make (prop, includeXType, skipGetter) {
        var attr = '[' + prop + '=',
            getter = 'get' + prop.charAt(0).toUpperCase() + prop.substring(1);
 
        Strategy.prototype.extractors[prop] = function (cmp, targets) {
            var value;
 
            if (skipGetter || prop in cmp) {
                value = cmp[prop];
            }
            else if (cmp[getter]) {
                // If stateId is equal id locator is not able to find a match
                if (cmp[getter]() !== cmp['id']) {
                    value = cmp[getter]();
                }
            }
 
            if (value || value === 0) {
                if (typeof(value) === 'string') {
                    value = value.replace(/,/g, '\\,');
                }
                if (targets) {
                    // some configs (like text) could have quotes
                    targets.push([
                        cmp.el.dom,
                        (includeXType ? this.getXType(cmp) : '') +
                            attr + JSON.stringify(value) + ']'
                    ]);
                }
 
                return true;
            }
        };
    }
 
    make('reference', true);
    make('itemId', true, true);
    make('name', true, true);
    make('stateId');
    make('text', true);
    make('boxLabel', true, false);
    make('fieldLabel', true);
    make('label', true);
});